@twinfinity/twin 6.0.1 → 6.0.2

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.
Files changed (97) hide show
  1. package/dist/BuildInfo.js +3 -3
  2. package/dist/TwinClient.d.ts +227 -0
  3. package/dist/TwinClient.d.ts.map +1 -1
  4. package/dist/TwinClient.js +474 -28
  5. package/dist/TwinClient.js.map +1 -1
  6. package/dist/errors.d.ts +122 -3
  7. package/dist/errors.d.ts.map +1 -1
  8. package/dist/errors.js +156 -0
  9. package/dist/errors.js.map +1 -1
  10. package/dist/tsconfig.tsbuildinfo +1 -1
  11. package/dist/types/asset-attachment-api.d.ts +51 -0
  12. package/dist/types/asset-attachment-api.d.ts.map +1 -0
  13. package/dist/types/asset-attachment-api.js +8 -0
  14. package/dist/types/asset-attachment-api.js.map +1 -0
  15. package/dist/types/asset-types-api.d.ts +76 -0
  16. package/dist/types/asset-types-api.d.ts.map +1 -0
  17. package/dist/types/asset-types-api.js +5 -0
  18. package/dist/types/asset-types-api.js.map +1 -0
  19. package/dist/types/index.d.ts +3 -0
  20. package/dist/types/index.d.ts.map +1 -1
  21. package/dist/types/index.js +3 -0
  22. package/dist/types/index.js.map +1 -1
  23. package/dist/types/twin-permissions-api.d.ts +20 -0
  24. package/dist/types/twin-permissions-api.d.ts.map +1 -0
  25. package/dist/types/twin-permissions-api.js +2 -0
  26. package/dist/types/twin-permissions-api.js.map +1 -0
  27. package/docs/assets/hierarchy.js +1 -1
  28. package/docs/assets/navigation.js +1 -1
  29. package/docs/assets/search.js +1 -1
  30. package/docs/classes/AssetAttachmentConflictError.html +3 -0
  31. package/docs/classes/AssetAttachmentNotFoundError.html +2 -0
  32. package/docs/classes/AssetBatchError.html +1 -1
  33. package/docs/classes/AssetCatalogTooLargeError.html +16 -0
  34. package/docs/classes/AssetCategoryConflictError.html +3 -0
  35. package/docs/classes/AssetCategoryForbiddenError.html +2 -0
  36. package/docs/classes/AssetCategoryNotFoundError.html +2 -0
  37. package/docs/classes/AssetCategoryValidationError.html +3 -0
  38. package/docs/classes/AssetConflictError.html +1 -1
  39. package/docs/classes/AssetForbiddenError.html +1 -1
  40. package/docs/classes/AssetNotFoundError.html +1 -1
  41. package/docs/classes/AssetTypeConflictError.html +3 -0
  42. package/docs/classes/AssetTypeForbiddenError.html +2 -0
  43. package/docs/classes/AssetTypeNotFoundError.html +2 -0
  44. package/docs/classes/AssetTypeValidationError.html +3 -0
  45. package/docs/classes/AssetValidationError.html +1 -1
  46. package/docs/classes/ProcessorNotFoundError.html +1 -1
  47. package/docs/classes/TwinClient.html +154 -48
  48. package/docs/classes/TwinConflictError.html +1 -1
  49. package/docs/classes/TwinFileConflictError.html +1 -1
  50. package/docs/classes/TwinFileNotFoundError.html +1 -1
  51. package/docs/classes/TwinFileValidationError.html +1 -1
  52. package/docs/classes/TwinNotFoundError.html +1 -1
  53. package/docs/classes/TwinUnauthenticatedError.html +6 -0
  54. package/docs/classes/TwinValidationError.html +1 -1
  55. package/docs/functions/isAssetCatalogTooLargeErrorBody.html +6 -0
  56. package/docs/functions/readErrorBody.html +10 -0
  57. package/docs/hierarchy.html +1 -1
  58. package/docs/interfaces/ApiCreateUploadSessionResponse.html +7 -0
  59. package/docs/interfaces/AssetAttachmentPut.html +6 -0
  60. package/docs/interfaces/AssetCatalogCategory.html +7 -0
  61. package/docs/interfaces/AssetCatalogResponse.html +6 -0
  62. package/docs/interfaces/AssetCatalogTooLargeErrorBody.html +15 -0
  63. package/docs/interfaces/AssetCategoryDelete.html +2 -0
  64. package/docs/interfaces/AssetCategoryGet.html +14 -0
  65. package/docs/interfaces/AssetCategoryPut.html +7 -0
  66. package/docs/interfaces/AssetTypeDelete.html +2 -0
  67. package/docs/interfaces/AssetTypeGet.html +16 -0
  68. package/docs/interfaces/AssetTypePut.html +9 -0
  69. package/docs/interfaces/AttachmentGet.html +20 -0
  70. package/docs/interfaces/AttachmentPost.html +4 -0
  71. package/docs/interfaces/ITwinClient.html +155 -49
  72. package/docs/interfaces/MyPermissions.html +7 -0
  73. package/docs/interfaces/TwinErrorBody.html +2 -2
  74. package/docs/interfaces/TwinSpecification.html +1 -1
  75. package/docs/interfaces/UploadStatusGet.html +3 -0
  76. package/docs/modules.html +1 -1
  77. package/docs/types/TwinCapability.html +6 -0
  78. package/docs/types/TwinErrorCause.html +8 -0
  79. package/docs/types/TwinErrorItemKind.html +4 -0
  80. package/docs/types/UploadStatus.html +1 -0
  81. package/package.json +3 -3
  82. package/src/BuildInfo.ts +41 -41
  83. package/src/TwinClient.ts +2334 -1496
  84. package/src/errors.ts +379 -117
  85. package/src/index.ts +3 -3
  86. package/src/types/Twin.ts +7 -7
  87. package/src/types/TwinGeometryInfo.ts +7 -7
  88. package/src/types/asset-api.ts +60 -60
  89. package/src/types/asset-attachment-api.ts +56 -0
  90. package/src/types/asset-batch-api.ts +88 -88
  91. package/src/types/asset-types-api.ts +86 -0
  92. package/src/types/common.ts +10 -10
  93. package/src/types/generated.ts +574 -574
  94. package/src/types/index.ts +9 -6
  95. package/src/types/twin-api.ts +99 -99
  96. package/src/types/twin-file-api.ts +67 -67
  97. package/src/types/twin-permissions-api.ts +20 -0
@@ -1,12 +1,15 @@
1
1
  import { HttpMethod } from '@twinfinity/authentication';
2
2
  import { readT8ClientModelWithFactories, readT8ClientModelWithGeometry, readT8GIModelWithFactories, T8EntityType, T8GIMeshDescriptor, T8GIStyle, T8GIRegion, T8Product, T8Project, T8Unit } from '@twinfinity/t8-client-model';
3
- import { TwinValidationError, TwinConflictError, TwinNotFoundError, ProcessorNotFoundError, AssetValidationError, AssetForbiddenError, AssetConflictError, AssetNotFoundError, AssetBatchError, TwinFileValidationError, TwinFileConflictError, TwinFileNotFoundError } from './errors';
3
+ import { TwinValidationError, TwinConflictError, TwinNotFoundError, ProcessorNotFoundError, AssetValidationError, AssetForbiddenError, AssetConflictError, AssetNotFoundError, AssetAttachmentConflictError, AssetAttachmentNotFoundError, AssetBatchError, TwinFileValidationError, TwinFileConflictError, TwinFileNotFoundError, AssetCategoryValidationError, AssetCategoryConflictError, AssetCategoryNotFoundError, AssetCategoryForbiddenError, AssetTypeValidationError, AssetTypeConflictError, AssetTypeNotFoundError, AssetTypeForbiddenError, AssetCatalogTooLargeError, isAssetCatalogTooLargeErrorBody, TwinUnauthenticatedError, readErrorBody } from './errors';
4
4
  export class TwinClient {
5
5
  _httpClient;
6
6
  _baseUrl;
7
+ _assetTypeCatalogUrl;
7
8
  constructor(baseUrl, _httpClient) {
8
9
  this._httpClient = _httpClient;
9
- this._baseUrl = `${baseUrl}/_ps/twins`;
10
+ const base = String(baseUrl).replace(/\/+$/, '');
11
+ this._baseUrl = `${base}/_ps/twins`;
12
+ this._assetTypeCatalogUrl = `${base}/_ps/asset-type-catalog`;
10
13
  }
11
14
  async listTwins(relatedTo, page = undefined, limit = 100, signal) {
12
15
  const params = new URLSearchParams();
@@ -18,14 +21,14 @@ export class TwinClient {
18
21
  params.set('limit', limit.toString());
19
22
  const url = params.toString() ? `${this._baseUrl}?${params}` : this._baseUrl;
20
23
  const response = await this._httpClient.fetch(HttpMethod.Get, url, {
21
- headers: { 'Content-Type': 'application/json' },
24
+ headers: { Accept: 'application/json' },
22
25
  signal
23
26
  });
24
27
  if (response.ok) {
25
28
  return (await response.json());
26
29
  }
27
30
  if (response.status === 400) {
28
- const errorBody = await response.json();
31
+ const errorBody = await readErrorBody(response);
29
32
  throw new TwinValidationError(`Twin listing validation failed: ${errorBody.detail}`, errorBody);
30
33
  }
31
34
  const errorText = await response.text();
@@ -50,7 +53,7 @@ export class TwinClient {
50
53
  const twinId = typeof twin === 'string' ? twin : twin.id;
51
54
  const specifiedVersion = typeof twin === 'string' ? undefined : twin.version;
52
55
  const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}${specifiedVersion !== undefined ? `/versions/${specifiedVersion}` : ''}`, {
53
- headers: { 'Content-Type': 'application/json' },
56
+ headers: { Accept: 'application/json' },
54
57
  signal
55
58
  });
56
59
  if (response.ok) {
@@ -72,14 +75,14 @@ export class TwinClient {
72
75
  params.set('includeDeleted', 'true');
73
76
  const url = `${this._baseUrl}/${id}/versions${params.toString() ? `?${params}` : ''}`;
74
77
  const response = await this._httpClient.fetch(HttpMethod.Get, url, {
75
- headers: { 'Content-Type': 'application/json' },
78
+ headers: { Accept: 'application/json' },
76
79
  signal
77
80
  });
78
81
  if (response.ok) {
79
82
  return (await response.json());
80
83
  }
81
84
  if (response.status === 400) {
82
- const errorBody = await response.json();
85
+ const errorBody = await readErrorBody(response);
83
86
  throw new TwinValidationError(`Twin listing validation failed: ${errorBody.detail}`, errorBody);
84
87
  }
85
88
  const errorText = await response.text();
@@ -243,7 +246,7 @@ export class TwinClient {
243
246
  return (await response.json());
244
247
  }
245
248
  if (response.status === 400) {
246
- const errorBody = await response.json();
249
+ const errorBody = await readErrorBody(response);
247
250
  throw new TwinValidationError(`Twin validation failed: ${errorBody.detail}`, errorBody);
248
251
  }
249
252
  if (response.status === 409) {
@@ -295,7 +298,7 @@ export class TwinClient {
295
298
  throw new Error(`Failed to restore twin: ${response.status} ${response.statusText} - ${errorText}`);
296
299
  }
297
300
  async getTwinVersionContent(twinId, twinVersion, signal) {
298
- const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/versions/${twinVersion}/contents`, { headers: { 'Content-Type': 'application/json' }, signal });
301
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/versions/${twinVersion}/contents`, { headers: { Accept: 'application/json' }, signal });
299
302
  if (response.ok) {
300
303
  return (await response.json());
301
304
  }
@@ -307,7 +310,7 @@ export class TwinClient {
307
310
  }
308
311
  async getTwinContent(twinId, signal) {
309
312
  const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/contents`, {
310
- headers: { 'Content-Type': 'application/json' },
313
+ headers: { Accept: 'application/json' },
311
314
  signal
312
315
  });
313
316
  if (response.ok) {
@@ -321,7 +324,7 @@ export class TwinClient {
321
324
  }
322
325
  async getTwinDiagnostics(twinId, signal) {
323
326
  const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/diagnostics`, {
324
- headers: { 'Content-Type': 'application/json' },
327
+ headers: { Accept: 'application/json' },
325
328
  signal
326
329
  });
327
330
  if (response.ok) {
@@ -334,7 +337,7 @@ export class TwinClient {
334
337
  throw new Error(`Failed to get twin diagnostics: ${response.status} ${response.statusText} - ${errorText}`);
335
338
  }
336
339
  async getTwinVersionDiagnostics(twinId, twinVersion, signal) {
337
- const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/versions/${twinVersion}/diagnostics`, { headers: { 'Content-Type': 'application/json' }, signal });
340
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/versions/${twinVersion}/diagnostics`, { headers: { Accept: 'application/json' }, signal });
338
341
  if (response.ok) {
339
342
  return (await response.json());
340
343
  }
@@ -347,14 +350,14 @@ export class TwinClient {
347
350
  async getTwinVersionEvents(twinId, twinVersion, offset = 0, limit = 100, signal) {
348
351
  const url = `${this._baseUrl}/${twinId}/versions/${twinVersion}/events?offset=${offset}&limit=${limit}`;
349
352
  const response = await this._httpClient.fetch(HttpMethod.Get, url, {
350
- headers: { 'Content-Type': 'application/json' },
353
+ headers: { Accept: 'application/json' },
351
354
  signal
352
355
  });
353
356
  if (response.ok) {
354
357
  return (await response.json());
355
358
  }
356
359
  if (response.status === 400) {
357
- const errorBody = await response.json();
360
+ const errorBody = await readErrorBody(response);
358
361
  throw new TwinValidationError(`Twin events validation failed: ${errorBody.detail}`, errorBody);
359
362
  }
360
363
  if (response.status === 404) {
@@ -366,14 +369,14 @@ export class TwinClient {
366
369
  async getProcessorEvents(twinId, twinVersion, processorId, offset = 0, limit = 100, signal) {
367
370
  const url = `${this._baseUrl}/${twinId}/versions/${twinVersion}/processors/${processorId}/events?offset=${offset}&limit=${limit}`;
368
371
  const response = await this._httpClient.fetch(HttpMethod.Get, url, {
369
- headers: { 'Content-Type': 'application/json' },
372
+ headers: { Accept: 'application/json' },
370
373
  signal
371
374
  });
372
375
  if (response.ok) {
373
376
  return (await response.json());
374
377
  }
375
378
  if (response.status === 400) {
376
- const errorBody = await response.json();
379
+ const errorBody = await readErrorBody(response);
377
380
  throw new TwinValidationError(`Processor events validation failed: ${errorBody.detail}`, errorBody);
378
381
  }
379
382
  if (response.status === 404) {
@@ -386,14 +389,14 @@ export class TwinClient {
386
389
  const includeDeleted = options?.includeDeleted ?? false;
387
390
  const params = `offset=${offset}&limit=${limit}${includeDeleted ? '&includeDeleted=true' : ''}`;
388
391
  const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/assets?${params}`, {
389
- headers: { 'Content-Type': 'application/json' },
392
+ headers: { Accept: 'application/json' },
390
393
  signal
391
394
  });
392
395
  if (response.ok) {
393
396
  return (await response.json());
394
397
  }
395
398
  if (response.status === 400) {
396
- const errorBody = await response.json();
399
+ const errorBody = await readErrorBody(response);
397
400
  throw new AssetValidationError(`Asset listing validation failed: ${errorBody.detail}`, errorBody);
398
401
  }
399
402
  if (response.status === 404) {
@@ -404,7 +407,7 @@ export class TwinClient {
404
407
  }
405
408
  async getAsset(twinId, assetId, signal) {
406
409
  const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/assets/${assetId}`, {
407
- headers: { 'Content-Type': 'application/json' },
410
+ headers: { Accept: 'application/json' },
408
411
  signal
409
412
  });
410
413
  if (response.ok) {
@@ -426,7 +429,7 @@ export class TwinClient {
426
429
  return (await response.json());
427
430
  }
428
431
  if (response.status === 400) {
429
- const errorBody = await response.json();
432
+ const errorBody = await readErrorBody(response);
430
433
  throw new AssetValidationError(`Asset validation failed: ${errorBody.detail}`, errorBody);
431
434
  }
432
435
  const errorText = await response.text();
@@ -446,7 +449,7 @@ export class TwinClient {
446
449
  throw new AssetConflictError('Asset has been modified by another user', conflictedAsset);
447
450
  }
448
451
  if (response.status === 400) {
449
- const errorBody = await response.json();
452
+ const errorBody = await readErrorBody(response);
450
453
  throw new AssetValidationError(`Asset validation failed: ${errorBody.detail}`, errorBody);
451
454
  }
452
455
  const errorText = await response.text();
@@ -499,7 +502,7 @@ export class TwinClient {
499
502
  return (await response.json());
500
503
  }
501
504
  if (response.status === 400) {
502
- const errorBody = await response.json();
505
+ const errorBody = await readErrorBody(response);
503
506
  throw new AssetValidationError(`Asset batch validation failed: ${errorBody.detail}`, errorBody);
504
507
  }
505
508
  if (response.status === 403) {
@@ -523,12 +526,163 @@ export class TwinClient {
523
526
  return (await response.json());
524
527
  }
525
528
  if (response.status === 400) {
526
- const errorBody = await response.json();
529
+ const errorBody = await readErrorBody(response);
527
530
  throw new AssetValidationError(`Asset batch-get validation failed: ${errorBody.detail}`, errorBody);
528
531
  }
529
532
  const errorText = await response.text();
530
533
  throw new Error(`Failed to batch-get assets: ${response.status} ${response.statusText} - ${errorText}`);
531
534
  }
535
+ // ────────── Asset attachments ──────────
536
+ async listAssetAttachments(twinId, assetId, options, signal) {
537
+ const params = new URLSearchParams();
538
+ if (options?.includeDeleted)
539
+ params.set('includeDeleted', 'true');
540
+ if (options?.offset !== undefined)
541
+ params.set('offset', String(options.offset));
542
+ if (options?.limit !== undefined)
543
+ params.set('limit', String(options.limit));
544
+ if (options?.page)
545
+ params.set('page', options.page);
546
+ const url = `${this._baseUrl}/${twinId}/assets/${assetId}/attachments${params.toString() ? `?${params}` : ''}`;
547
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
548
+ headers: { Accept: 'application/json' },
549
+ signal
550
+ });
551
+ if (response.ok)
552
+ return (await response.json());
553
+ if (response.status === 404)
554
+ throw new AssetNotFoundError(`Asset ${assetId} not found`);
555
+ if (response.status === 400) {
556
+ const errorBody = await readErrorBody(response);
557
+ throw new AssetValidationError(`Attachment listing failed: ${errorBody.detail}`, errorBody);
558
+ }
559
+ const errorText = await response.text();
560
+ throw new Error(`Failed to list attachments: ${response.status} ${response.statusText} - ${errorText}`);
561
+ }
562
+ async getAssetAttachmentBlob(twinId, assetId, attachmentId, signal) {
563
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/assets/${assetId}/attachments/${attachmentId}`, { signal });
564
+ if (response.ok)
565
+ return response;
566
+ if (response.status === 404)
567
+ throw new AssetAttachmentNotFoundError(`Attachment ${attachmentId} not found`);
568
+ const errorText = await response.text();
569
+ throw new Error(`Failed to fetch attachment blob: ${response.status} ${response.statusText} - ${errorText}`);
570
+ }
571
+ async createAssetAttachmentUploadSession(twinId, assetId, attachmentId, body, signal) {
572
+ const response = await this._httpClient.fetch(HttpMethod.Post, `${this._baseUrl}/${twinId}/assets/${assetId}/attachments/${attachmentId}/createAttachmentUploadSession`, {
573
+ headers: { 'Content-Type': 'application/json' },
574
+ body: JSON.stringify(body),
575
+ signal
576
+ });
577
+ if (response.ok)
578
+ return (await response.json());
579
+ if (response.status === 409) {
580
+ const conflicted = (await response.json());
581
+ throw new AssetAttachmentConflictError(`Attachment ${attachmentId} already exists; pick a new id for retries`, conflicted);
582
+ }
583
+ if (response.status === 404)
584
+ throw new AssetNotFoundError(`Asset ${assetId} not found`);
585
+ if (response.status === 400) {
586
+ const errorBody = await readErrorBody(response);
587
+ throw new AssetValidationError(`Upload session creation failed: ${errorBody.detail}`, errorBody);
588
+ }
589
+ const errorText = await response.text();
590
+ throw new Error(`Failed to create upload session: ${response.status} ${response.statusText} - ${errorText}`);
591
+ }
592
+ async uploadAssetAttachmentFile(session, file, signal) {
593
+ // The token is encoded as a query param on the upload URL; we lift it
594
+ // out for the caller so they can poll uploadStatus without re-parsing.
595
+ const tokenMatch = session.uploadUrl.match(/token=([^&]+)/);
596
+ if (!tokenMatch) {
597
+ throw new Error(`Upload session URL is missing a token query parameter: ${session.uploadUrl}`);
598
+ }
599
+ const token = tokenMatch[1];
600
+ const chunkSize = Math.max(1, session.maxChunkSize || file.size);
601
+ const total = file.size;
602
+ let offset = 0;
603
+ // Even an empty file needs at least one PUT so the upload session
604
+ // gets a finalize signal. uploadChunks() in the integration tests
605
+ // unconditionally PUTs at least one chunk too.
606
+ do {
607
+ const end = Math.min(offset + chunkSize, total);
608
+ const chunk = file.slice(offset, end);
609
+ const lastByte = Math.max(0, end - 1);
610
+ const response = await this._httpClient.fetch(HttpMethod.Put, session.uploadUrl, {
611
+ headers: { 'Content-Range': `bytes ${offset}-${lastByte}/${total}` },
612
+ body: chunk,
613
+ signal
614
+ });
615
+ if (!response.ok) {
616
+ const errorText = await response.text();
617
+ throw new Error(`Upload chunk failed at offset ${offset}: ${response.status} ${response.statusText} - ${errorText}`);
618
+ }
619
+ offset = end;
620
+ } while (offset < total);
621
+ return { token };
622
+ }
623
+ async getAssetAttachmentUploadStatus(twinId, assetId, token, signal) {
624
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/assets/${assetId}/attachments/uploadStatus/${encodeURIComponent(token)}`, { headers: { Accept: 'application/json' }, signal });
625
+ if (response.ok)
626
+ return (await response.json());
627
+ if (response.status === 404)
628
+ return { status: 'NotFound' };
629
+ const errorText = await response.text();
630
+ throw new Error(`Failed to get upload status: ${response.status} ${response.statusText} - ${errorText}`);
631
+ }
632
+ async putAssetAttachment(twinId, assetId, attachmentId, body, signal) {
633
+ const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}/assets/${assetId}/attachments/${attachmentId}`, {
634
+ headers: { 'Content-Type': 'application/json' },
635
+ body: JSON.stringify(body),
636
+ signal
637
+ });
638
+ if (response.ok)
639
+ return (await response.json());
640
+ if (response.status === 409) {
641
+ const conflicted = (await response.json());
642
+ throw new AssetAttachmentConflictError('Attachment has been modified by another user', conflicted);
643
+ }
644
+ if (response.status === 404)
645
+ throw new AssetAttachmentNotFoundError(`Attachment ${attachmentId} not found`);
646
+ if (response.status === 400) {
647
+ const errorBody = await readErrorBody(response);
648
+ throw new AssetValidationError(`Attachment update failed: ${errorBody.detail}`, errorBody);
649
+ }
650
+ const errorText = await response.text();
651
+ throw new Error(`Failed to update attachment: ${response.status} ${response.statusText} - ${errorText}`);
652
+ }
653
+ async deleteAssetAttachment(twinId, assetId, attachmentId, etag, signal) {
654
+ const response = await this._httpClient.fetch(HttpMethod.Delete, `${this._baseUrl}/${twinId}/assets/${assetId}/attachments/${attachmentId}`, {
655
+ headers: { 'If-Match': etag },
656
+ signal
657
+ });
658
+ if (response.ok)
659
+ return (await response.json());
660
+ if (response.status === 409) {
661
+ const conflicted = (await response.json());
662
+ throw new AssetAttachmentConflictError('Attachment has been modified by another user', conflicted);
663
+ }
664
+ if (response.status === 404)
665
+ throw new AssetAttachmentNotFoundError(`Attachment ${attachmentId} not found`);
666
+ const errorText = await response.text();
667
+ throw new Error(`Failed to delete attachment: ${response.status} ${response.statusText} - ${errorText}`);
668
+ }
669
+ async restoreAssetAttachment(twinId, assetId, attachmentId, etag, signal) {
670
+ const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}/assets/${assetId}/attachments/${attachmentId}/restore`, {
671
+ headers: { 'Content-Type': 'application/json' },
672
+ body: JSON.stringify({ etag }),
673
+ signal
674
+ });
675
+ if (response.ok)
676
+ return (await response.json());
677
+ if (response.status === 409) {
678
+ const conflicted = (await response.json());
679
+ throw new AssetAttachmentConflictError('Attachment has been modified by another user', conflicted);
680
+ }
681
+ if (response.status === 404)
682
+ throw new AssetAttachmentNotFoundError(`Attachment ${attachmentId} not found`);
683
+ const errorText = await response.text();
684
+ throw new Error(`Failed to restore attachment: ${response.status} ${response.statusText} - ${errorText}`);
685
+ }
532
686
  async listTwinFiles(twinId, options = {}, signal) {
533
687
  const { includeDeleted = false, checkedIn = false, version, page = undefined, limit = 100 } = options;
534
688
  if (version !== undefined && !checkedIn) {
@@ -549,14 +703,14 @@ export class TwinClient {
549
703
  ? `${this._baseUrl}/${twinId}/files?${params}`
550
704
  : `${this._baseUrl}/${twinId}/files`;
551
705
  const response = await this._httpClient.fetch(HttpMethod.Get, url, {
552
- headers: { 'Content-Type': 'application/json' },
706
+ headers: { Accept: 'application/json' },
553
707
  signal
554
708
  });
555
709
  if (response.ok) {
556
710
  return (await response.json());
557
711
  }
558
712
  if (response.status === 400) {
559
- const errorBody = await response.json();
713
+ const errorBody = await readErrorBody(response);
560
714
  throw new TwinFileValidationError(`Twin file listing validation failed: ${errorBody.detail}`, errorBody);
561
715
  }
562
716
  const errorText = await response.text();
@@ -564,7 +718,7 @@ export class TwinClient {
564
718
  }
565
719
  async getTwinFile(twinId, fileId, signal) {
566
720
  const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/files/${fileId}`, {
567
- headers: { 'Content-Type': 'application/json' },
721
+ headers: { Accept: 'application/json' },
568
722
  signal
569
723
  });
570
724
  if (response.ok) {
@@ -590,7 +744,7 @@ export class TwinClient {
590
744
  return (await response.json());
591
745
  }
592
746
  if (response.status === 400) {
593
- const errorBody = await response.json();
747
+ const errorBody = await readErrorBody(response);
594
748
  throw new TwinFileValidationError(`Twin file validation failed`, errorBody);
595
749
  }
596
750
  if (response.status === 409) {
@@ -681,7 +835,7 @@ export class TwinClient {
681
835
  return (await response.json());
682
836
  }
683
837
  if (response.status === 400) {
684
- const errorBody = await response.json();
838
+ const errorBody = await readErrorBody(response);
685
839
  throw new TwinFileValidationError(`Twin file version listing validation failed: ${errorBody.detail}`, errorBody);
686
840
  }
687
841
  const errorText = await response.text();
@@ -712,5 +866,297 @@ export class TwinClient {
712
866
  signal
713
867
  });
714
868
  }
869
+ // ---------- Asset categories ----------
870
+ async getMyPermissions(signal) {
871
+ const url = `${this._baseUrl}/permissions/me`;
872
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
873
+ headers: { Accept: 'application/json' },
874
+ signal
875
+ });
876
+ if (response.ok) {
877
+ return (await response.json());
878
+ }
879
+ // 401: no valid auth context. Throw so consumers can redirect to
880
+ // login / refresh tokens rather than mistake an expired session for
881
+ // "logged-in user with no grants."
882
+ if (response.status === 401) {
883
+ throw new TwinUnauthenticatedError('Not authenticated');
884
+ }
885
+ // 403: authenticated but no Twin-service capabilities. Surface as an
886
+ // empty array so consumers can treat it like a logged-in user with
887
+ // zero grants.
888
+ if (response.status === 403) {
889
+ return { permissions: [] };
890
+ }
891
+ throw new Error(`Failed to fetch permissions: ${response.status} ${response.statusText}`);
892
+ }
893
+ async getAssetTypeMetaSchema(signal) {
894
+ const url = `${this._assetTypeCatalogUrl}/meta-schema`;
895
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
896
+ headers: { Accept: 'application/schema+json, application/json' },
897
+ signal
898
+ });
899
+ if (!response.ok) {
900
+ throw new Error(`Failed to fetch meta-schema: ${response.status} ${response.statusText}`);
901
+ }
902
+ return (await response.json());
903
+ }
904
+ async listAssetCategories(options, signal) {
905
+ const params = new URLSearchParams();
906
+ if (options?.page !== undefined)
907
+ params.set('page', options.page);
908
+ if (options?.limit !== undefined)
909
+ params.set('limit', options.limit.toString());
910
+ if (options?.includeDeleted)
911
+ params.set('includeDeleted', 'true');
912
+ const url = params.toString()
913
+ ? `${this._assetTypeCatalogUrl}/categories?${params}`
914
+ : `${this._assetTypeCatalogUrl}/categories`;
915
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
916
+ headers: { Accept: 'application/json' },
917
+ signal
918
+ });
919
+ if (response.ok)
920
+ return (await response.json());
921
+ if (response.status === 403)
922
+ throw new AssetCategoryForbiddenError('Asset category listing forbidden');
923
+ if (response.status === 400) {
924
+ const errorBody = await readErrorBody(response);
925
+ throw new AssetCategoryValidationError(`Asset category listing failed: ${errorBody.detail}`, errorBody);
926
+ }
927
+ throw new Error(`Failed to list asset categories: ${response.status} ${response.statusText}`);
928
+ }
929
+ async getAssetCategory(categoryId, signal) {
930
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._assetTypeCatalogUrl}/categories/${categoryId}`, {
931
+ headers: { Accept: 'application/json' },
932
+ signal
933
+ });
934
+ if (response.ok)
935
+ return (await response.json());
936
+ if (response.status === 403)
937
+ throw new AssetCategoryForbiddenError(`Asset category ${categoryId} read forbidden`);
938
+ if (response.status === 404)
939
+ throw new AssetCategoryNotFoundError(`Asset category ${categoryId} not found`);
940
+ throw new Error(`Failed to get asset category: ${response.status} ${response.statusText}`);
941
+ }
942
+ async putAssetCategory(categoryId, body, signal) {
943
+ const response = await this._httpClient.fetch(HttpMethod.Put, `${this._assetTypeCatalogUrl}/categories/${categoryId}`, {
944
+ headers: { 'Content-Type': 'application/json' },
945
+ body: JSON.stringify(body),
946
+ signal
947
+ });
948
+ if (response.ok)
949
+ return (await response.json());
950
+ if (response.status === 403)
951
+ throw new AssetCategoryForbiddenError('Asset category upsert forbidden');
952
+ if (response.status === 409) {
953
+ const current = (await response.json());
954
+ throw new AssetCategoryConflictError('Asset category was modified by another user', current);
955
+ }
956
+ if (response.status === 400) {
957
+ const errorBody = await readErrorBody(response);
958
+ throw new AssetCategoryValidationError(`Asset category validation failed: ${errorBody.detail}`, errorBody);
959
+ }
960
+ const errorText = await response.text();
961
+ throw new Error(`Failed to upsert asset category: ${response.status} ${response.statusText} - ${errorText}`);
962
+ }
963
+ async deleteAssetCategory(categoryId, etag, signal) {
964
+ return this._categoryDeleteOrRestore(categoryId, etag, /*isRestore*/ false, signal);
965
+ }
966
+ async restoreAssetCategory(categoryId, etag, signal) {
967
+ return this._categoryDeleteOrRestore(categoryId, etag, /*isRestore*/ true, signal);
968
+ }
969
+ async _categoryDeleteOrRestore(categoryId, etag, isRestore, signal) {
970
+ const url = isRestore
971
+ ? `${this._assetTypeCatalogUrl}/categories/${categoryId}/restore`
972
+ : `${this._assetTypeCatalogUrl}/categories/${categoryId}`;
973
+ const method = isRestore ? HttpMethod.Put : HttpMethod.Delete;
974
+ const response = await this._httpClient.fetch(method, url, {
975
+ headers: { 'Content-Type': 'application/json' },
976
+ body: JSON.stringify({ etag }),
977
+ signal
978
+ });
979
+ if (response.ok)
980
+ return (await response.json());
981
+ if (response.status === 403) {
982
+ const op = isRestore ? 'restore' : 'delete';
983
+ throw new AssetCategoryForbiddenError(`Asset category ${op} forbidden`);
984
+ }
985
+ if (response.status === 409) {
986
+ const current = (await response.json());
987
+ throw new AssetCategoryConflictError('Asset category was modified by another user', current);
988
+ }
989
+ if (response.status === 404)
990
+ throw new AssetCategoryNotFoundError(`Asset category ${categoryId} not found`);
991
+ const errorText = await response.text();
992
+ const op = isRestore ? 'restore' : 'delete';
993
+ throw new Error(`Failed to ${op} asset category: ${response.status} ${response.statusText} - ${errorText}`);
994
+ }
995
+ async listAssetCategoryVersions(categoryId, options, signal) {
996
+ const params = new URLSearchParams();
997
+ if (options?.page !== undefined)
998
+ params.set('page', options.page);
999
+ if (options?.limit !== undefined)
1000
+ params.set('limit', options.limit.toString());
1001
+ if (options?.includeDeleted)
1002
+ params.set('includeDeleted', 'true');
1003
+ const baseUrl = `${this._assetTypeCatalogUrl}/categories/${categoryId}/versions`;
1004
+ const url = params.toString() ? `${baseUrl}?${params}` : baseUrl;
1005
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
1006
+ headers: { Accept: 'application/json' },
1007
+ signal
1008
+ });
1009
+ if (response.ok)
1010
+ return (await response.json());
1011
+ if (response.status === 403)
1012
+ throw new AssetCategoryForbiddenError('Asset category version listing forbidden');
1013
+ if (response.status === 400) {
1014
+ const errorBody = await readErrorBody(response);
1015
+ throw new AssetCategoryValidationError(`Asset category version listing failed: ${errorBody.detail}`, errorBody);
1016
+ }
1017
+ if (response.status === 404)
1018
+ throw new AssetCategoryNotFoundError(`Asset category ${categoryId} not found`);
1019
+ throw new Error(`Failed to list asset category versions: ${response.status} ${response.statusText}`);
1020
+ }
1021
+ // ---------- Asset types ----------
1022
+ async listAssetTypes(categoryId, options, signal) {
1023
+ const params = new URLSearchParams();
1024
+ if (options?.page !== undefined)
1025
+ params.set('page', options.page);
1026
+ if (options?.limit !== undefined)
1027
+ params.set('limit', options.limit.toString());
1028
+ if (options?.includeDeleted)
1029
+ params.set('includeDeleted', 'true');
1030
+ const baseUrl = `${this._assetTypeCatalogUrl}/categories/${categoryId}/types`;
1031
+ const url = params.toString() ? `${baseUrl}?${params}` : baseUrl;
1032
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
1033
+ headers: { Accept: 'application/json' },
1034
+ signal
1035
+ });
1036
+ if (response.ok)
1037
+ return (await response.json());
1038
+ if (response.status === 403)
1039
+ throw new AssetTypeForbiddenError('Asset type listing forbidden');
1040
+ if (response.status === 400) {
1041
+ const errorBody = await readErrorBody(response);
1042
+ throw new AssetTypeValidationError(`Asset type listing failed: ${errorBody.detail}`, errorBody);
1043
+ }
1044
+ throw new Error(`Failed to list asset types: ${response.status} ${response.statusText}`);
1045
+ }
1046
+ async getAssetType(categoryId, typeId, signal) {
1047
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._assetTypeCatalogUrl}/categories/${categoryId}/types/${typeId}`, { headers: { Accept: 'application/json' }, signal });
1048
+ if (response.ok)
1049
+ return (await response.json());
1050
+ if (response.status === 403)
1051
+ throw new AssetTypeForbiddenError(`Asset type ${typeId} read forbidden`);
1052
+ if (response.status === 404)
1053
+ throw new AssetTypeNotFoundError(`Asset type ${typeId} not found`);
1054
+ throw new Error(`Failed to get asset type: ${response.status} ${response.statusText}`);
1055
+ }
1056
+ async listAssetTypeVersions(categoryId, typeId, options, signal) {
1057
+ const params = new URLSearchParams();
1058
+ if (options?.page !== undefined)
1059
+ params.set('page', options.page);
1060
+ if (options?.limit !== undefined)
1061
+ params.set('limit', options.limit.toString());
1062
+ if (options?.includeDeleted)
1063
+ params.set('includeDeleted', 'true');
1064
+ const baseUrl = `${this._assetTypeCatalogUrl}/categories/${categoryId}/types/${typeId}/versions`;
1065
+ const url = params.toString() ? `${baseUrl}?${params}` : baseUrl;
1066
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
1067
+ headers: { Accept: 'application/json' },
1068
+ signal
1069
+ });
1070
+ if (response.ok)
1071
+ return (await response.json());
1072
+ if (response.status === 403)
1073
+ throw new AssetTypeForbiddenError('Asset type version listing forbidden');
1074
+ if (response.status === 400) {
1075
+ const errorBody = await readErrorBody(response);
1076
+ throw new AssetTypeValidationError(`Asset type version listing failed: ${errorBody.detail}`, errorBody);
1077
+ }
1078
+ if (response.status === 404)
1079
+ throw new AssetTypeNotFoundError(`Asset type ${typeId} not found`);
1080
+ throw new Error(`Failed to list asset type versions: ${response.status} ${response.statusText}`);
1081
+ }
1082
+ async putAssetType(categoryId, typeId, body, signal) {
1083
+ const response = await this._httpClient.fetch(HttpMethod.Put, `${this._assetTypeCatalogUrl}/categories/${categoryId}/types/${typeId}`, {
1084
+ headers: { 'Content-Type': 'application/json' },
1085
+ body: JSON.stringify(body),
1086
+ signal
1087
+ });
1088
+ if (response.ok)
1089
+ return (await response.json());
1090
+ if (response.status === 403)
1091
+ throw new AssetTypeForbiddenError('Asset type upsert forbidden');
1092
+ if (response.status === 409) {
1093
+ const current = (await response.json());
1094
+ throw new AssetTypeConflictError('Asset type was modified by another user', current);
1095
+ }
1096
+ if (response.status === 400) {
1097
+ const errorBody = await readErrorBody(response);
1098
+ throw new AssetTypeValidationError(`Asset type validation failed: ${errorBody.detail}`, errorBody);
1099
+ }
1100
+ const errorText = await response.text();
1101
+ throw new Error(`Failed to upsert asset type: ${response.status} ${response.statusText} - ${errorText}`);
1102
+ }
1103
+ async deleteAssetType(categoryId, typeId, etag, signal) {
1104
+ return this._typeDeleteOrRestore(categoryId, typeId, etag, /*isRestore*/ false, signal);
1105
+ }
1106
+ async restoreAssetType(categoryId, typeId, etag, signal) {
1107
+ return this._typeDeleteOrRestore(categoryId, typeId, etag, /*isRestore*/ true, signal);
1108
+ }
1109
+ async _typeDeleteOrRestore(categoryId, typeId, etag, isRestore, signal) {
1110
+ const url = isRestore
1111
+ ? `${this._assetTypeCatalogUrl}/categories/${categoryId}/types/${typeId}/restore`
1112
+ : `${this._assetTypeCatalogUrl}/categories/${categoryId}/types/${typeId}`;
1113
+ const method = isRestore ? HttpMethod.Put : HttpMethod.Delete;
1114
+ const response = await this._httpClient.fetch(method, url, {
1115
+ headers: { 'Content-Type': 'application/json' },
1116
+ body: JSON.stringify({ etag }),
1117
+ signal
1118
+ });
1119
+ if (response.ok)
1120
+ return (await response.json());
1121
+ if (response.status === 403) {
1122
+ const op = isRestore ? 'restore' : 'delete';
1123
+ throw new AssetTypeForbiddenError(`Asset type ${op} forbidden`);
1124
+ }
1125
+ if (response.status === 409) {
1126
+ const current = (await response.json());
1127
+ throw new AssetTypeConflictError('Asset type was modified by another user', current);
1128
+ }
1129
+ if (response.status === 404)
1130
+ throw new AssetTypeNotFoundError(`Asset type ${typeId} not found`);
1131
+ const errorText = await response.text();
1132
+ const op = isRestore ? 'restore' : 'delete';
1133
+ throw new Error(`Failed to ${op} asset type: ${response.status} ${response.statusText} - ${errorText}`);
1134
+ }
1135
+ async getAssetTypeCatalog(signal) {
1136
+ const response = await this._httpClient.fetch(HttpMethod.Get, this._assetTypeCatalogUrl, {
1137
+ headers: { Accept: 'application/json' },
1138
+ signal
1139
+ });
1140
+ if (response.ok)
1141
+ return (await response.json());
1142
+ if (response.status === 403)
1143
+ throw new AssetCategoryForbiddenError('Asset type catalog read forbidden');
1144
+ if (response.status === 400) {
1145
+ // The server emits a single structured 400 here today —
1146
+ // `cause: 'CatalogTooLarge'` — to flag a tenant whose catalog
1147
+ // overflows the per-customer cap. Surface as a typed error so
1148
+ // picker UIs can render "use search" instead of crashing on
1149
+ // missing rows. Any other 400 (malformed CatalogTooLarge body,
1150
+ // future cause) falls through to the generic error below so the
1151
+ // caller still sees the raw response.
1152
+ const errorBody = await readErrorBody(response);
1153
+ if (isAssetCatalogTooLargeErrorBody(errorBody)) {
1154
+ throw new AssetCatalogTooLargeError(`Asset type catalog too large: ${errorBody.detail}`, errorBody);
1155
+ }
1156
+ throw new Error(`Failed to get asset-type catalog: ${response.status} ${response.statusText} - ${errorBody.detail}`);
1157
+ }
1158
+ const errorText = await response.text();
1159
+ throw new Error(`Failed to get asset-type catalog: ${response.status} ${response.statusText} - ${errorText}`);
1160
+ }
715
1161
  }
716
1162
  //# sourceMappingURL=TwinClient.js.map