@twinfinity/twin 0.0.0-feature.31596

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 (245) hide show
  1. package/README.md +277 -0
  2. package/dist/BuildInfo.d.ts +25 -0
  3. package/dist/BuildInfo.d.ts.map +1 -0
  4. package/dist/BuildInfo.js +15 -0
  5. package/dist/BuildInfo.js.map +1 -0
  6. package/dist/TwinClient.d.ts +823 -0
  7. package/dist/TwinClient.d.ts.map +1 -0
  8. package/dist/TwinClient.js +1487 -0
  9. package/dist/TwinClient.js.map +1 -0
  10. package/dist/errors.d.ts +289 -0
  11. package/dist/errors.d.ts.map +1 -0
  12. package/dist/errors.js +357 -0
  13. package/dist/errors.js.map +1 -0
  14. package/dist/index.d.ts +4 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +4 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/tsconfig.tsbuildinfo +1 -0
  19. package/dist/types/Twin.d.ts +7 -0
  20. package/dist/types/Twin.d.ts.map +1 -0
  21. package/dist/types/Twin.js +6 -0
  22. package/dist/types/Twin.js.map +1 -0
  23. package/dist/types/TwinGeometryInfo.d.ts +7 -0
  24. package/dist/types/TwinGeometryInfo.d.ts.map +1 -0
  25. package/dist/types/TwinGeometryInfo.js +2 -0
  26. package/dist/types/TwinGeometryInfo.js.map +1 -0
  27. package/dist/types/asset-api.d.ts +66 -0
  28. package/dist/types/asset-api.d.ts.map +1 -0
  29. package/dist/types/asset-api.js +2 -0
  30. package/dist/types/asset-api.js.map +1 -0
  31. package/dist/types/asset-attachment-api.d.ts +51 -0
  32. package/dist/types/asset-attachment-api.d.ts.map +1 -0
  33. package/dist/types/asset-attachment-api.js +8 -0
  34. package/dist/types/asset-attachment-api.js.map +1 -0
  35. package/dist/types/asset-batch-api.d.ts +73 -0
  36. package/dist/types/asset-batch-api.d.ts.map +1 -0
  37. package/dist/types/asset-batch-api.js +2 -0
  38. package/dist/types/asset-batch-api.js.map +1 -0
  39. package/dist/types/asset-types-api.d.ts +76 -0
  40. package/dist/types/asset-types-api.d.ts.map +1 -0
  41. package/dist/types/asset-types-api.js +5 -0
  42. package/dist/types/asset-types-api.js.map +1 -0
  43. package/dist/types/common.d.ts +10 -0
  44. package/dist/types/common.d.ts.map +1 -0
  45. package/dist/types/common.js +2 -0
  46. package/dist/types/common.js.map +1 -0
  47. package/dist/types/generated.d.ts +458 -0
  48. package/dist/types/generated.d.ts.map +1 -0
  49. package/dist/types/generated.js +2 -0
  50. package/dist/types/generated.js.map +1 -0
  51. package/dist/types/index.d.ts +10 -0
  52. package/dist/types/index.d.ts.map +1 -0
  53. package/dist/types/index.js +10 -0
  54. package/dist/types/index.js.map +1 -0
  55. package/dist/types/twin-api.d.ts +141 -0
  56. package/dist/types/twin-api.d.ts.map +1 -0
  57. package/dist/types/twin-api.js +2 -0
  58. package/dist/types/twin-api.js.map +1 -0
  59. package/dist/types/twin-file-api.d.ts +61 -0
  60. package/dist/types/twin-file-api.d.ts.map +1 -0
  61. package/dist/types/twin-file-api.js +2 -0
  62. package/dist/types/twin-file-api.js.map +1 -0
  63. package/dist/types/twin-permissions-api.d.ts +240 -0
  64. package/dist/types/twin-permissions-api.d.ts.map +1 -0
  65. package/dist/types/twin-permissions-api.js +2 -0
  66. package/dist/types/twin-permissions-api.js.map +1 -0
  67. package/docs/.nojekyll +1 -0
  68. package/docs/assets/hierarchy.js +1 -0
  69. package/docs/assets/highlight.css +99 -0
  70. package/docs/assets/icons.js +18 -0
  71. package/docs/assets/icons.svg +1 -0
  72. package/docs/assets/main.js +60 -0
  73. package/docs/assets/navigation.js +1 -0
  74. package/docs/assets/search.js +1 -0
  75. package/docs/assets/style.css +1633 -0
  76. package/docs/classes/AssetAttachmentConflictError.html +3 -0
  77. package/docs/classes/AssetAttachmentNotFoundError.html +2 -0
  78. package/docs/classes/AssetBatchError.html +3 -0
  79. package/docs/classes/AssetCatalogTooLargeError.html +16 -0
  80. package/docs/classes/AssetCategoryConflictError.html +3 -0
  81. package/docs/classes/AssetCategoryForbiddenError.html +2 -0
  82. package/docs/classes/AssetCategoryNotFoundError.html +2 -0
  83. package/docs/classes/AssetCategoryValidationError.html +3 -0
  84. package/docs/classes/AssetConflictError.html +3 -0
  85. package/docs/classes/AssetForbiddenError.html +2 -0
  86. package/docs/classes/AssetNotFoundError.html +2 -0
  87. package/docs/classes/AssetPermissionsConflictError.html +16 -0
  88. package/docs/classes/AssetTypeConflictError.html +3 -0
  89. package/docs/classes/AssetTypeForbiddenError.html +2 -0
  90. package/docs/classes/AssetTypeNotFoundError.html +2 -0
  91. package/docs/classes/AssetTypeValidationError.html +3 -0
  92. package/docs/classes/AssetValidationError.html +3 -0
  93. package/docs/classes/ProcessorNotFoundError.html +2 -0
  94. package/docs/classes/TwinClient.html +409 -0
  95. package/docs/classes/TwinConflictError.html +3 -0
  96. package/docs/classes/TwinFileConflictError.html +3 -0
  97. package/docs/classes/TwinFileNotFoundError.html +2 -0
  98. package/docs/classes/TwinFileValidationError.html +3 -0
  99. package/docs/classes/TwinNotFoundError.html +4 -0
  100. package/docs/classes/TwinPermissionsConflictError.html +19 -0
  101. package/docs/classes/TwinPermissionsForbiddenError.html +12 -0
  102. package/docs/classes/TwinPrincipalNotFoundError.html +5 -0
  103. package/docs/classes/TwinTemporarilyUnavailableError.html +10 -0
  104. package/docs/classes/TwinUnauthenticatedError.html +6 -0
  105. package/docs/classes/TwinValidationError.html +3 -0
  106. package/docs/functions/isAssetCatalogTooLargeErrorBody.html +6 -0
  107. package/docs/functions/readErrorBody.html +10 -0
  108. package/docs/hierarchy.html +1 -0
  109. package/docs/index.html +168 -0
  110. package/docs/interfaces/ApiCreateUploadSessionResponse.html +7 -0
  111. package/docs/interfaces/AssetAttachmentPut.html +6 -0
  112. package/docs/interfaces/AssetBatchDeleteOperation.html +4 -0
  113. package/docs/interfaces/AssetBatchGetRequest.html +2 -0
  114. package/docs/interfaces/AssetBatchGetResponse.html +3 -0
  115. package/docs/interfaces/AssetBatchRequest.html +2 -0
  116. package/docs/interfaces/AssetBatchResponse.html +3 -0
  117. package/docs/interfaces/AssetBatchRestoreOperation.html +4 -0
  118. package/docs/interfaces/AssetBatchUpsertItem.html +12 -0
  119. package/docs/interfaces/AssetBatchUpsertOperation.html +4 -0
  120. package/docs/interfaces/AssetCatalogCategory.html +7 -0
  121. package/docs/interfaces/AssetCatalogResponse.html +6 -0
  122. package/docs/interfaces/AssetCatalogTooLargeErrorBody.html +15 -0
  123. package/docs/interfaces/AssetCategoryDelete.html +2 -0
  124. package/docs/interfaces/AssetCategoryGet.html +14 -0
  125. package/docs/interfaces/AssetCategoryPut.html +7 -0
  126. package/docs/interfaces/AssetGet.html +18 -0
  127. package/docs/interfaces/AssetGroupGet.html +9 -0
  128. package/docs/interfaces/AssetGroupsGet.html +3 -0
  129. package/docs/interfaces/AssetListItem.html +14 -0
  130. package/docs/interfaces/AssetPermissionsConflictErrorBody.html +12 -0
  131. package/docs/interfaces/AssetPermissionsPut.html +9 -0
  132. package/docs/interfaces/AssetPosition.html +4 -0
  133. package/docs/interfaces/AssetPost.html +12 -0
  134. package/docs/interfaces/AssetPrincipalPermissionsGet.html +23 -0
  135. package/docs/interfaces/AssetPut.html +12 -0
  136. package/docs/interfaces/AssetTypeDelete.html +2 -0
  137. package/docs/interfaces/AssetTypeGet.html +16 -0
  138. package/docs/interfaces/AssetTypePut.html +9 -0
  139. package/docs/interfaces/AttachmentGet.html +20 -0
  140. package/docs/interfaces/AttachmentPost.html +4 -0
  141. package/docs/interfaces/BatchItemError.html +4 -0
  142. package/docs/interfaces/BatchItemResult.html +6 -0
  143. package/docs/interfaces/BatchSummary.html +4 -0
  144. package/docs/interfaces/Coordinates.html +6 -0
  145. package/docs/interfaces/Diagnostics.html +4 -0
  146. package/docs/interfaces/DiagnosticsSummary.html +4 -0
  147. package/docs/interfaces/ElementQuantities.html +3 -0
  148. package/docs/interfaces/EntityCollection.html +15 -0
  149. package/docs/interfaces/EntityRepresentation.html +12 -0
  150. package/docs/interfaces/Facility.html +22 -0
  151. package/docs/interfaces/FacilityPart.html +18 -0
  152. package/docs/interfaces/FilePatch.html +7 -0
  153. package/docs/interfaces/FilePatchSet.html +5 -0
  154. package/docs/interfaces/GenericDiagnosticEntry.html +4 -0
  155. package/docs/interfaces/ITwinClient.html +404 -0
  156. package/docs/interfaces/IfcDiagnosticEntry.html +5 -0
  157. package/docs/interfaces/IfcDiagnosticNode.html +6 -0
  158. package/docs/interfaces/IfcDiagnosticNodeData.html +6 -0
  159. package/docs/interfaces/InheritedPermissions.html +8 -0
  160. package/docs/interfaces/ListTwinItem.html +30 -0
  161. package/docs/interfaces/Location.html +13 -0
  162. package/docs/interfaces/MyPermissions.html +7 -0
  163. package/docs/interfaces/MyTwinPermissions.html +5 -0
  164. package/docs/interfaces/PaginatedResponse.html +3 -0
  165. package/docs/interfaces/Pagination.html +4 -0
  166. package/docs/interfaces/Property.html +4 -0
  167. package/docs/interfaces/PropertySet.html +6 -0
  168. package/docs/interfaces/SimpleQuantity.html +6 -0
  169. package/docs/interfaces/SinglePropertyValue.html +5 -0
  170. package/docs/interfaces/Site.html +27 -0
  171. package/docs/interfaces/Space.html +16 -0
  172. package/docs/interfaces/TwinComputedPropertySetsResponse.html +4 -0
  173. package/docs/interfaces/TwinContentResponse.html +6 -0
  174. package/docs/interfaces/TwinDelete.html +2 -0
  175. package/docs/interfaces/TwinDiagnosticEntry.html +5 -0
  176. package/docs/interfaces/TwinDiagnosticNode.html +4 -0
  177. package/docs/interfaces/TwinDiagnosticNodeData.html +9 -0
  178. package/docs/interfaces/TwinDiagnosticsResponse.html +5 -0
  179. package/docs/interfaces/TwinErrorBody.html +4 -0
  180. package/docs/interfaces/TwinFileDelete.html +2 -0
  181. package/docs/interfaces/TwinFileGet.html +15 -0
  182. package/docs/interfaces/TwinFileListOptions.html +6 -0
  183. package/docs/interfaces/TwinFilePut.html +8 -0
  184. package/docs/interfaces/TwinFileReference.html +4 -0
  185. package/docs/interfaces/TwinFileRestore.html +2 -0
  186. package/docs/interfaces/TwinFileSource.html +4 -0
  187. package/docs/interfaces/TwinFileTransform.html +11 -0
  188. package/docs/interfaces/TwinFileVersionListOptions.html +4 -0
  189. package/docs/interfaces/TwinGet.html +48 -0
  190. package/docs/interfaces/TwinGroupGet.html +10 -0
  191. package/docs/interfaces/TwinGroupsGet.html +3 -0
  192. package/docs/interfaces/TwinPermissionsConflictErrorBody.html +8 -0
  193. package/docs/interfaces/TwinPermissionsPut.html +11 -0
  194. package/docs/interfaces/TwinPrincipalPermissionsGet.html +34 -0
  195. package/docs/interfaces/TwinProcessorEvent.html +7 -0
  196. package/docs/interfaces/TwinProcessorResponse.html +5 -0
  197. package/docs/interfaces/TwinPut.html +21 -0
  198. package/docs/interfaces/TwinReference.html +4 -0
  199. package/docs/interfaces/TwinSpecification.html +18 -0
  200. package/docs/interfaces/TwinVersionContentResponse.html +7 -0
  201. package/docs/interfaces/UploadStatusGet.html +3 -0
  202. package/docs/media/twin-management.md +399 -0
  203. package/docs/modules.html +1 -0
  204. package/docs/types/AssetBatchGetStatus.html +1 -0
  205. package/docs/types/AssetBatchOperation.html +1 -0
  206. package/docs/types/AssetBatchOperationStatus.html +1 -0
  207. package/docs/types/DiagnosticCategory.html +2 -0
  208. package/docs/types/DiagnosticLevel.html +2 -0
  209. package/docs/types/EpsgType.html +1 -0
  210. package/docs/types/FacilityClass.html +3 -0
  211. package/docs/types/FacilityPartClass.html +3 -0
  212. package/docs/types/IfcDiagnosticCode.html +1 -0
  213. package/docs/types/Op.html +2 -0
  214. package/docs/types/SinglePropertyValueType.html +3 -0
  215. package/docs/types/SingleQuantityValueType.html +3 -0
  216. package/docs/types/SiteType.html +3 -0
  217. package/docs/types/SpaceType.html +2 -0
  218. package/docs/types/TwinBoundCapabilityName.html +8 -0
  219. package/docs/types/TwinCapability.html +12 -0
  220. package/docs/types/TwinCapabilityName.html +8 -0
  221. package/docs/types/TwinDiagnosticCode.html +1 -0
  222. package/docs/types/TwinErrorCause.html +8 -0
  223. package/docs/types/TwinErrorItemKind.html +4 -0
  224. package/docs/types/TwinPermissionsConflictCause.html +6 -0
  225. package/docs/types/TwinScopedCapability.html +9 -0
  226. package/docs/types/TwinScopedCapabilityName.html +8 -0
  227. package/docs/types/UploadStatus.html +1 -0
  228. package/package.json +52 -0
  229. package/src/BuildInfo.ts +41 -0
  230. package/src/TwinClient.ts +3113 -0
  231. package/src/errors.ts +567 -0
  232. package/src/index.ts +3 -0
  233. package/src/tsconfig.json +13 -0
  234. package/src/types/Twin.ts +7 -0
  235. package/src/types/TwinGeometryInfo.ts +7 -0
  236. package/src/types/asset-api.ts +69 -0
  237. package/src/types/asset-attachment-api.ts +56 -0
  238. package/src/types/asset-batch-api.ts +90 -0
  239. package/src/types/asset-types-api.ts +86 -0
  240. package/src/types/common.ts +10 -0
  241. package/src/types/generated.ts +655 -0
  242. package/src/types/index.ts +9 -0
  243. package/src/types/twin-api.ts +153 -0
  244. package/src/types/twin-file-api.ts +67 -0
  245. package/src/types/twin-permissions-api.ts +257 -0
@@ -0,0 +1,1487 @@
1
+ import { HttpMethod } from '@twinfinity/authentication';
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, AssetAttachmentConflictError, AssetAttachmentNotFoundError, AssetBatchError, TwinFileValidationError, TwinFileConflictError, TwinFileNotFoundError, AssetCategoryValidationError, AssetCategoryConflictError, AssetCategoryNotFoundError, AssetCategoryForbiddenError, AssetTypeValidationError, AssetTypeConflictError, AssetTypeNotFoundError, AssetTypeForbiddenError, AssetCatalogTooLargeError, isAssetCatalogTooLargeErrorBody, TwinUnauthenticatedError, TwinPermissionsConflictError, TwinPermissionsForbiddenError, TwinPrincipalNotFoundError, TwinTemporarilyUnavailableError, AssetPermissionsConflictError, readErrorBody } from './errors';
4
+ /**
5
+ * The `Retry-After` of a 503, in seconds, when the response carried a usable one. The Twin service
6
+ * sends a delta-seconds value; an HTTP-date is permitted by RFC 7231 and simply yields nothing here
7
+ * rather than a nonsense delay.
8
+ */
9
+ function retryAfterSeconds(response) {
10
+ const header = response.headers?.get('Retry-After');
11
+ if (header === null || header === undefined)
12
+ return undefined;
13
+ const seconds = Number(header);
14
+ return Number.isInteger(seconds) && seconds >= 0 ? seconds : undefined;
15
+ }
16
+ /**
17
+ * The batch response body carried by a failed batch, or `undefined` when the body is not one.
18
+ *
19
+ * A rolled-back batch answers with per-item results under the status of its *last failing item* --
20
+ * so the same endpoint returns a batch body under 400, 404 or 409, while a whole-request rejection
21
+ * (an empty batch, duplicate ids, a body/URL twin mismatch, oversized metadata) returns problem
22
+ * details under 400 as well. Status alone cannot tell those apart, and branching on it reported
23
+ * every per-item 400 -- a cross-twin id, a deleted asset, metadata failing its schema -- as a
24
+ * validation failure with no results for the caller to inspect.
25
+ */
26
+ async function readBatchResponse(response) {
27
+ // Parse a clone: json() consumes the stream, and a body that turns out not to be a batch
28
+ // response still has to be readable as problem details or as text by the caller. Without
29
+ // clone -- some test doubles -- the probe is skipped rather than risk consuming the body.
30
+ if (typeof response.clone !== 'function')
31
+ return undefined;
32
+ try {
33
+ const parsed = (await response.clone().json());
34
+ return Array.isArray(parsed?.results) ? parsed : undefined;
35
+ }
36
+ catch {
37
+ return undefined;
38
+ }
39
+ }
40
+ export class TwinClient {
41
+ _httpClient;
42
+ _baseUrl;
43
+ _assetTypeCatalogUrl;
44
+ constructor(baseUrl, _httpClient) {
45
+ this._httpClient = _httpClient;
46
+ const base = String(baseUrl).replace(/\/+$/, '');
47
+ this._baseUrl = `${base}/_ps/twins`;
48
+ this._assetTypeCatalogUrl = `${base}/_ps/asset-type-catalog`;
49
+ }
50
+ /**
51
+ * Rooted twins base URL — typically `<host>/sites/<container>/_ps/twins`.
52
+ * Exposed so callers that want to build content URLs in the same auth
53
+ * scope as the metadata calls can derive them from here. The bare
54
+ * `<host>/_ps/twins/...` variant is not necessarily an authorized route
55
+ * on all deployments.
56
+ */
57
+ get baseUrl() {
58
+ return this._baseUrl;
59
+ }
60
+ async listTwins(optionsOrRelatedTo, pageOrSignal, positionalLimit, positionalSignal) {
61
+ let relatedTo;
62
+ let includeDeleted = false;
63
+ let page;
64
+ let limit = 100;
65
+ let signal;
66
+ if (typeof optionsOrRelatedTo === 'string') {
67
+ relatedTo = optionsOrRelatedTo;
68
+ page = pageOrSignal;
69
+ limit = positionalLimit ?? 100;
70
+ signal = positionalSignal;
71
+ }
72
+ else {
73
+ ({ relatedTo, includeDeleted = false, page = undefined, limit = 100 } = optionsOrRelatedTo ?? {});
74
+ signal = pageOrSignal;
75
+ }
76
+ const params = new URLSearchParams();
77
+ if (relatedTo)
78
+ params.set('relatedTo', relatedTo);
79
+ if (page !== undefined)
80
+ params.set('page', page);
81
+ if (limit !== 100)
82
+ params.set('limit', limit.toString());
83
+ if (includeDeleted)
84
+ params.set('includeDeleted', 'true');
85
+ const url = params.toString() ? `${this._baseUrl}?${params}` : this._baseUrl;
86
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
87
+ headers: { Accept: 'application/json' },
88
+ signal
89
+ });
90
+ if (response.ok) {
91
+ return (await response.json());
92
+ }
93
+ if (response.status === 400) {
94
+ const errorBody = await readErrorBody(response);
95
+ throw new TwinValidationError(`Twin listing validation failed: ${errorBody.detail}`, errorBody);
96
+ }
97
+ const errorText = await response.text();
98
+ throw new Error(`Failed to list twins: ${response.status} ${response.statusText} - ${errorText}`);
99
+ }
100
+ async listAllTwins(optionsOrRelatedTo, signalArg) {
101
+ let relatedTo;
102
+ let includeDeleted = false;
103
+ let signal;
104
+ if (typeof optionsOrRelatedTo === 'string') {
105
+ relatedTo = optionsOrRelatedTo;
106
+ signal = signalArg;
107
+ }
108
+ else {
109
+ ({ relatedTo, includeDeleted = false } = optionsOrRelatedTo ?? {});
110
+ signal = signalArg;
111
+ }
112
+ const allTwins = [];
113
+ let page = undefined;
114
+ const limit = 100;
115
+ const maxPages = 1000;
116
+ for (let pageCount = 0; pageCount < maxPages; pageCount++) {
117
+ const response = await this.listTwins({ relatedTo, page, limit, includeDeleted }, signal);
118
+ allTwins.push(...response.data);
119
+ if (response.data.length < limit) {
120
+ break;
121
+ }
122
+ page = response.pagination.nextPage;
123
+ }
124
+ return allTwins;
125
+ }
126
+ async getTwin(twin, signal) {
127
+ const twinId = typeof twin === 'string' ? twin : twin.id;
128
+ const specifiedVersion = typeof twin === 'string' ? undefined : twin.version;
129
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}${specifiedVersion !== undefined ? `/versions/${specifiedVersion}` : ''}`, {
130
+ headers: { Accept: 'application/json' },
131
+ signal
132
+ });
133
+ if (response.ok) {
134
+ return (await response.json());
135
+ }
136
+ if (response.status === 404) {
137
+ throw new TwinNotFoundError(`Twin ${twinId} not found`);
138
+ }
139
+ const errorText = await response.text();
140
+ throw new Error(`Failed to get twin: ${response.status} ${response.statusText} - ${errorText}`);
141
+ }
142
+ async listTwinVersions(id, options, signal) {
143
+ const params = new URLSearchParams();
144
+ if (options?.page !== undefined)
145
+ params.set('page', options.page);
146
+ if (options?.limit !== undefined && options.limit !== 100)
147
+ params.set('limit', options.limit.toString());
148
+ if (options?.includeDeleted)
149
+ params.set('includeDeleted', 'true');
150
+ const url = `${this._baseUrl}/${id}/versions${params.toString() ? `?${params}` : ''}`;
151
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
152
+ headers: { Accept: 'application/json' },
153
+ signal
154
+ });
155
+ if (response.ok) {
156
+ return (await response.json());
157
+ }
158
+ if (response.status === 400) {
159
+ const errorBody = await readErrorBody(response);
160
+ throw new TwinValidationError(`Twin listing validation failed: ${errorBody.detail}`, errorBody);
161
+ }
162
+ const errorText = await response.text();
163
+ throw new Error(`Failed to list twins: ${response.status} ${response.statusText} - ${errorText}`);
164
+ }
165
+ async downloadTwinContent(twinId, twinVersion, content, signal) {
166
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/versions/${twinVersion}/contents/${content.id}/content`, { signal });
167
+ if (response.ok) {
168
+ const blob = await response.blob();
169
+ return await blob.arrayBuffer();
170
+ }
171
+ throw new Error(`Failed to download twin content: ${response.status} ${response.statusText}`);
172
+ }
173
+ async loadTwinVersion(twinId, twinVersion, projectFactory, productFactory, entityTypeFactory, unitFactory, connectProject, connectProduct, signal) {
174
+ const twinContent = await this.getTwinVersionContent(twinId, twinVersion, signal);
175
+ const t8cmItem = twinContent.content.find((item) => item.format === 't8cm');
176
+ if (t8cmItem === undefined) {
177
+ return undefined;
178
+ }
179
+ const t8cmExtensions = twinContent.content.filter((item) => item.format === 't8cm-ext');
180
+ const data = await Promise.all([this.downloadTwinContent(twinId, twinVersion, t8cmItem, signal)].concat(t8cmExtensions.map((ext) => this.downloadTwinContent(twinId, twinVersion, ext, signal))));
181
+ return readT8ClientModelWithFactories(data, projectFactory, productFactory, entityTypeFactory, unitFactory, connectProject, connectProduct);
182
+ }
183
+ loadTwin(twin, signal) {
184
+ return this.loadTwinWithFactories(twin, (T8Project.create), (T8Product.create), T8EntityType.create, T8Unit.create, T8Project.connect, T8Product.connect, signal);
185
+ }
186
+ async loadTwinWithFactories(twin, projectFactory, productFactory, entityTypeFactory, unitFactory, connectProject, connectProduct, signal) {
187
+ let page = undefined;
188
+ const twinId = typeof twin === 'string' ? twin : twin.id;
189
+ const specifiedVersion = typeof twin === 'string' ? undefined : twin.version;
190
+ if (specifiedVersion !== undefined) {
191
+ return {
192
+ id: twinId,
193
+ version: specifiedVersion,
194
+ model: await this.loadTwinVersion(twinId, specifiedVersion, projectFactory, productFactory, entityTypeFactory, unitFactory, connectProject, connectProduct, signal)
195
+ };
196
+ }
197
+ const maxPages = 100;
198
+ for (let pageCount = 0; pageCount < maxPages; pageCount++) {
199
+ const versions = await this.listTwinVersions(twinId, { page }, signal);
200
+ if (versions.data.length === 0) {
201
+ break;
202
+ }
203
+ for (const versionItem of versions.data) {
204
+ const loadedModel = await this.loadTwinVersion(twinId, versionItem.version, projectFactory, productFactory, entityTypeFactory, unitFactory, connectProject, connectProduct, signal);
205
+ if (loadedModel !== undefined) {
206
+ return {
207
+ id: twinId,
208
+ version: versionItem.version,
209
+ model: loadedModel
210
+ };
211
+ }
212
+ }
213
+ page = versions.pagination.nextPage;
214
+ }
215
+ throw new Error(`Twin ${twinId} does not have a client model`);
216
+ }
217
+ async loadGeometryInfoVersion(twinId, twinVersion, meshDescriptorFactory, styleFactory, regionFactory, signal) {
218
+ const twinContent = await this.getTwinVersionContent(twinId, twinVersion, signal);
219
+ const t8giItem = twinContent.content.find((item) => item.format === 't8gi');
220
+ if (t8giItem === undefined) {
221
+ return undefined;
222
+ }
223
+ const data = await this.downloadTwinContent(twinId, twinVersion, t8giItem, signal);
224
+ return readT8GIModelWithFactories(data, meshDescriptorFactory, styleFactory, regionFactory);
225
+ }
226
+ loadGeometryInfo(twin, signal) {
227
+ return this.loadGeometryInfoWithFactories(twin, T8GIMeshDescriptor.create, T8GIStyle.create, T8GIRegion.create, signal);
228
+ }
229
+ async loadGeometryInfoWithFactories(twin, meshDescriptorFactory, styleFactory, regionFactory, signal) {
230
+ let page = undefined;
231
+ const twinId = typeof twin === 'string' ? twin : twin.id;
232
+ const specifiedVersion = typeof twin === 'string' ? undefined : twin.version;
233
+ if (specifiedVersion !== undefined) {
234
+ return {
235
+ id: twinId,
236
+ version: specifiedVersion,
237
+ model: await this.loadGeometryInfoVersion(twinId, specifiedVersion, meshDescriptorFactory, styleFactory, regionFactory, signal)
238
+ };
239
+ }
240
+ const maxPages = 100;
241
+ for (let pageCount = 0; pageCount < maxPages; pageCount++) {
242
+ const versions = await this.listTwinVersions(twinId, { page }, signal);
243
+ if (versions.data.length === 0) {
244
+ break;
245
+ }
246
+ for (const versionItem of versions.data) {
247
+ const model = await this.loadGeometryInfoVersion(twinId, versionItem.version, meshDescriptorFactory, styleFactory, regionFactory, signal);
248
+ if (model !== undefined) {
249
+ return {
250
+ id: twinId,
251
+ version: versionItem.version,
252
+ model
253
+ };
254
+ }
255
+ }
256
+ page = versions.pagination.nextPage;
257
+ }
258
+ throw new Error(`Twin ${twinId} does not have geometry info`);
259
+ }
260
+ async loadTwinWithGeometryVersion(twinId, twinVersion, signal) {
261
+ const twinContent = await this.getTwinVersionContent(twinId, twinVersion, signal);
262
+ const t8cmItem = twinContent.content.find((item) => item.format === 't8cm');
263
+ const t8giItem = twinContent.content.find((item) => item.format === 't8gi');
264
+ if (t8cmItem === undefined || t8giItem === undefined) {
265
+ return undefined;
266
+ }
267
+ const t8cmExtensions = twinContent.content.filter((item) => item.format === 't8cm-ext');
268
+ const t8gdItem = twinContent.content.find((item) => item.format === 't8gd');
269
+ const downloads = [
270
+ this.downloadTwinContent(twinId, twinVersion, t8cmItem, signal),
271
+ ...t8cmExtensions.map((ext) => this.downloadTwinContent(twinId, twinVersion, ext, signal)),
272
+ this.downloadTwinContent(twinId, twinVersion, t8giItem, signal),
273
+ ...(t8gdItem ? [this.downloadTwinContent(twinId, twinVersion, t8gdItem, signal)] : [])
274
+ ];
275
+ const allData = await Promise.all(downloads);
276
+ const t8cmCount = 1 + t8cmExtensions.length;
277
+ const t8cmData = allData.slice(0, t8cmCount);
278
+ const t8giData = allData[t8cmCount];
279
+ const t8gdData = t8gdItem ? allData[t8cmCount + 1] : undefined;
280
+ return readT8ClientModelWithGeometry(t8cmData, t8giData, t8gdData);
281
+ }
282
+ async loadTwinWithGeometry(twin, signal) {
283
+ let page = undefined;
284
+ const twinId = typeof twin === 'string' ? twin : twin.id;
285
+ const specifiedVersion = typeof twin === 'string' ? undefined : twin.version;
286
+ if (specifiedVersion !== undefined) {
287
+ return {
288
+ id: twinId,
289
+ version: specifiedVersion,
290
+ model: await this.loadTwinWithGeometryVersion(twinId, specifiedVersion, signal)
291
+ };
292
+ }
293
+ const maxPages = 100;
294
+ for (let pageCount = 0; pageCount < maxPages; pageCount++) {
295
+ const versions = await this.listTwinVersions(twinId, { page }, signal);
296
+ if (versions.data.length === 0) {
297
+ break;
298
+ }
299
+ for (const versionItem of versions.data) {
300
+ const model = await this.loadTwinWithGeometryVersion(twinId, versionItem.version, signal);
301
+ if (model !== undefined) {
302
+ return {
303
+ id: twinId,
304
+ version: versionItem.version,
305
+ model
306
+ };
307
+ }
308
+ }
309
+ page = versions.pagination.nextPage;
310
+ }
311
+ throw new Error(`Twin ${twinId} does not have a client model with geometry`);
312
+ }
313
+ async createOrUpdateTwin(twinId, twin, signal) {
314
+ const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}`, {
315
+ headers: { 'Content-Type': 'application/json' },
316
+ body: JSON.stringify(twin),
317
+ signal
318
+ });
319
+ if (response.ok) {
320
+ return (await response.json());
321
+ }
322
+ if (response.status === 400) {
323
+ const errorBody = await readErrorBody(response);
324
+ throw new TwinValidationError(`Twin validation failed: ${errorBody.detail}`, errorBody);
325
+ }
326
+ if (response.status === 409) {
327
+ const conflictedTwin = (await response.json());
328
+ throw new TwinConflictError('Twin has been modified by another user', conflictedTwin);
329
+ }
330
+ const errorText = await response.text();
331
+ throw new Error(`Failed to create/update twin: ${response.status} ${response.statusText} - ${errorText}`);
332
+ }
333
+ async updateTwin(twinId, twin, signal) {
334
+ return this.createOrUpdateTwin(twinId, twin, signal);
335
+ }
336
+ async deleteTwin(twinId, etag, signal) {
337
+ const response = await this._httpClient.fetch(HttpMethod.Delete, `${this._baseUrl}/${twinId}`, {
338
+ headers: { 'Content-Type': 'application/json' },
339
+ body: JSON.stringify({ etag }),
340
+ signal
341
+ });
342
+ if (response.ok) {
343
+ return (await response.json());
344
+ }
345
+ if (response.status === 404) {
346
+ throw new TwinNotFoundError(`Twin ${twinId} not found`);
347
+ }
348
+ if (response.status === 409) {
349
+ const conflictedTwin = (await response.json());
350
+ throw new TwinConflictError('Twin has been modified by another user', conflictedTwin);
351
+ }
352
+ const errorText = await response.text();
353
+ throw new Error(`Failed to delete twin: ${response.status} ${response.statusText} - ${errorText}`);
354
+ }
355
+ async restoreTwin(twinId, etag, signal) {
356
+ const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}/restore`, {
357
+ headers: { 'Content-Type': 'application/json' },
358
+ body: JSON.stringify({ etag }),
359
+ signal
360
+ });
361
+ if (response.ok) {
362
+ return (await response.json());
363
+ }
364
+ if (response.status === 404) {
365
+ throw new TwinNotFoundError(`Twin ${twinId} not found`);
366
+ }
367
+ if (response.status === 409) {
368
+ const conflictedTwin = (await response.json());
369
+ throw new TwinConflictError('Twin has been modified by another user', conflictedTwin);
370
+ }
371
+ const errorText = await response.text();
372
+ throw new Error(`Failed to restore twin: ${response.status} ${response.statusText} - ${errorText}`);
373
+ }
374
+ async getTwinVersionContent(twinId, twinVersion, signal) {
375
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/versions/${twinVersion}/contents`, { headers: { Accept: 'application/json' }, signal });
376
+ if (response.ok) {
377
+ return (await response.json());
378
+ }
379
+ if (response.status === 404) {
380
+ throw new TwinNotFoundError(`Twin ${twinId} version ${twinVersion} not found`);
381
+ }
382
+ const errorText = await response.text();
383
+ throw new Error(`Failed to get twin version content: ${response.status} ${response.statusText} - ${errorText}`);
384
+ }
385
+ async getTwinContent(twinId, signal) {
386
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/contents`, {
387
+ headers: { Accept: 'application/json' },
388
+ signal
389
+ });
390
+ if (response.ok) {
391
+ return (await response.json());
392
+ }
393
+ if (response.status === 404) {
394
+ throw new TwinNotFoundError(`Twin ${twinId} not found`);
395
+ }
396
+ const errorText = await response.text();
397
+ throw new Error(`Failed to get twin content: ${response.status} ${response.statusText} - ${errorText}`);
398
+ }
399
+ async getTwinDiagnostics(twinId, signal) {
400
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/diagnostics`, {
401
+ headers: { Accept: 'application/json' },
402
+ signal
403
+ });
404
+ if (response.ok) {
405
+ return (await response.json());
406
+ }
407
+ if (response.status === 404) {
408
+ throw new TwinNotFoundError(`Twin ${twinId} not found`);
409
+ }
410
+ const errorText = await response.text();
411
+ throw new Error(`Failed to get twin diagnostics: ${response.status} ${response.statusText} - ${errorText}`);
412
+ }
413
+ async getTwinVersionDiagnostics(twinId, twinVersion, signal) {
414
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/versions/${twinVersion}/diagnostics`, { headers: { Accept: 'application/json' }, signal });
415
+ if (response.ok) {
416
+ return (await response.json());
417
+ }
418
+ if (response.status === 404) {
419
+ throw new TwinNotFoundError(`Twin ${twinId} version ${twinVersion} not found`);
420
+ }
421
+ const errorText = await response.text();
422
+ throw new Error(`Failed to get twin version diagnostics: ${response.status} ${response.statusText} - ${errorText}`);
423
+ }
424
+ async getTwinVersionEvents(twinId, twinVersion, offset = 0, limit = 100, signal) {
425
+ const url = `${this._baseUrl}/${twinId}/versions/${twinVersion}/events?offset=${offset}&limit=${limit}`;
426
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
427
+ headers: { Accept: 'application/json' },
428
+ signal
429
+ });
430
+ if (response.ok) {
431
+ return (await response.json());
432
+ }
433
+ if (response.status === 400) {
434
+ const errorBody = await readErrorBody(response);
435
+ throw new TwinValidationError(`Twin events validation failed: ${errorBody.detail}`, errorBody);
436
+ }
437
+ if (response.status === 404) {
438
+ throw new TwinNotFoundError(`Twin ${twinId} version ${twinVersion} not found`);
439
+ }
440
+ const errorText = await response.text();
441
+ throw new Error(`Failed to get twin version events: ${response.status} ${response.statusText} - ${errorText}`);
442
+ }
443
+ async getProcessorEvents(twinId, twinVersion, processorId, offset = 0, limit = 100, signal) {
444
+ const url = `${this._baseUrl}/${twinId}/versions/${twinVersion}/processors/${processorId}/events?offset=${offset}&limit=${limit}`;
445
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
446
+ headers: { Accept: 'application/json' },
447
+ signal
448
+ });
449
+ if (response.ok) {
450
+ return (await response.json());
451
+ }
452
+ if (response.status === 400) {
453
+ const errorBody = await readErrorBody(response);
454
+ throw new TwinValidationError(`Processor events validation failed: ${errorBody.detail}`, errorBody);
455
+ }
456
+ if (response.status === 404) {
457
+ throw new ProcessorNotFoundError(`Processor ${processorId} not found for twin ${twinId} version ${twinVersion}`);
458
+ }
459
+ const errorText = await response.text();
460
+ throw new Error(`Failed to get processor events: ${response.status} ${response.statusText} - ${errorText}`);
461
+ }
462
+ async listAssets(twinId, offset = 0, limit = 100, options, signal) {
463
+ const includeDeleted = options?.includeDeleted ?? false;
464
+ const params = `offset=${offset}&limit=${limit}${includeDeleted ? '&includeDeleted=true' : ''}`;
465
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/assets?${params}`, {
466
+ headers: { Accept: 'application/json' },
467
+ signal
468
+ });
469
+ if (response.ok) {
470
+ return (await response.json());
471
+ }
472
+ if (response.status === 400) {
473
+ const errorBody = await readErrorBody(response);
474
+ throw new AssetValidationError(`Asset listing validation failed: ${errorBody.detail}`, errorBody);
475
+ }
476
+ if (response.status === 404) {
477
+ throw new AssetNotFoundError(`Twin ${twinId} not found`);
478
+ }
479
+ const errorText = await response.text();
480
+ throw new Error(`Failed to list assets: ${response.status} ${response.statusText} - ${errorText}`);
481
+ }
482
+ async getAsset(twinId, assetId, signal) {
483
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/assets/${assetId}`, {
484
+ headers: { Accept: 'application/json' },
485
+ signal
486
+ });
487
+ if (response.ok) {
488
+ return (await response.json());
489
+ }
490
+ if (response.status === 404) {
491
+ throw new AssetNotFoundError(`Asset ${assetId} not found`);
492
+ }
493
+ const errorText = await response.text();
494
+ throw new Error(`Failed to get asset: ${response.status} ${response.statusText} - ${errorText}`);
495
+ }
496
+ async createAssets(twinId, assets, signal) {
497
+ const response = await this._httpClient.fetch(HttpMethod.Post, `${this._baseUrl}/${twinId}/assets`, {
498
+ headers: { 'Content-Type': 'application/json' },
499
+ body: JSON.stringify(assets),
500
+ signal
501
+ });
502
+ if (response.status === 201) {
503
+ return (await response.json());
504
+ }
505
+ if (response.status === 400) {
506
+ const errorBody = await readErrorBody(response);
507
+ throw new AssetValidationError(`Asset validation failed: ${errorBody.detail}`, errorBody);
508
+ }
509
+ const errorText = await response.text();
510
+ throw new Error(`Failed to create assets: ${response.status} ${response.statusText} - ${errorText}`);
511
+ }
512
+ async updateAsset(twinId, assetId, asset, signal) {
513
+ const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}/assets/${assetId}`, {
514
+ headers: { 'Content-Type': 'application/json' },
515
+ body: JSON.stringify(asset),
516
+ signal
517
+ });
518
+ if (response.ok) {
519
+ return (await response.json());
520
+ }
521
+ if (response.status === 409) {
522
+ const conflictedAsset = (await response.json());
523
+ throw new AssetConflictError('Asset has been modified by another user', conflictedAsset);
524
+ }
525
+ if (response.status === 400) {
526
+ const errorBody = await readErrorBody(response);
527
+ throw new AssetValidationError(`Asset validation failed: ${errorBody.detail}`, errorBody);
528
+ }
529
+ const errorText = await response.text();
530
+ throw new Error(`Failed to update asset: ${response.status} ${response.statusText} - ${errorText}`);
531
+ }
532
+ async deleteAsset(twinId, assetId, etag, signal) {
533
+ const response = await this._httpClient.fetch(HttpMethod.Delete, `${this._baseUrl}/${twinId}/assets/${assetId}`, {
534
+ headers: { 'Content-Type': 'application/json' },
535
+ body: JSON.stringify({ etag }),
536
+ signal
537
+ });
538
+ if (response.ok) {
539
+ return (await response.json());
540
+ }
541
+ if (response.status === 409) {
542
+ const conflictedAsset = (await response.json());
543
+ throw new AssetConflictError('Asset has been modified by another user', conflictedAsset);
544
+ }
545
+ if (response.status === 404) {
546
+ throw new AssetNotFoundError(`Asset ${assetId} not found`);
547
+ }
548
+ const errorText = await response.text();
549
+ throw new Error(`Failed to delete asset: ${response.status} ${response.statusText} - ${errorText}`);
550
+ }
551
+ async restoreAsset(twinId, assetId, etag) {
552
+ const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}/assets/${assetId}/restore`, {
553
+ headers: { 'Content-Type': 'application/json' },
554
+ body: JSON.stringify({ etag })
555
+ });
556
+ if (response.ok) {
557
+ return (await response.json());
558
+ }
559
+ if (response.status === 409) {
560
+ const conflictedAsset = (await response.json());
561
+ throw new AssetConflictError('Asset has been modified by another user', conflictedAsset);
562
+ }
563
+ if (response.status === 404) {
564
+ throw new AssetNotFoundError(`Asset ${assetId} not found`);
565
+ }
566
+ const errorText = await response.text();
567
+ throw new Error(`Failed to restore asset: ${response.status} ${response.statusText} - ${errorText}`);
568
+ }
569
+ async batchAssets(twinId, operations, signal) {
570
+ const response = await this._httpClient.fetch(HttpMethod.Post, `${this._baseUrl}/${twinId}/assets/batch`, {
571
+ headers: { 'Content-Type': 'application/json' },
572
+ body: JSON.stringify({ operations }),
573
+ signal
574
+ });
575
+ if (response.ok) {
576
+ return (await response.json());
577
+ }
578
+ // The body decides, not the status: a rolled-back batch is reported as a batch error at
579
+ // whatever status its last failing item carried, and only a body that is not a batch
580
+ // response falls through to the status arms below.
581
+ const batchResponse = await readBatchResponse(response);
582
+ if (batchResponse) {
583
+ throw new AssetBatchError(`Asset batch operation failed with status ${response.status}`, batchResponse);
584
+ }
585
+ if (response.status === 400) {
586
+ const errorBody = await readErrorBody(response);
587
+ throw new AssetValidationError(`Asset batch validation failed: ${errorBody.detail}`, errorBody);
588
+ }
589
+ if (response.status === 403) {
590
+ const errorText = await response.text();
591
+ throw new AssetForbiddenError(`Asset batch forbidden: ${errorText}`);
592
+ }
593
+ // Reached only where the probe above could not read the body -- a Response without clone(),
594
+ // which some test doubles are. These two statuses only ever carry a batch response, so read
595
+ // it directly rather than losing the per-item results to the generic error below.
596
+ if (response.status === 404 || response.status === 409) {
597
+ const rolledBack = (await response.json());
598
+ throw new AssetBatchError(`Asset batch operation failed with status ${response.status}`, rolledBack);
599
+ }
600
+ const errorText = await response.text();
601
+ throw new Error(`Failed to execute asset batch: ${response.status} ${response.statusText} - ${errorText}`);
602
+ }
603
+ async batchGetAssets(twinId, ids, signal) {
604
+ const response = await this._httpClient.fetch(HttpMethod.Post, `${this._baseUrl}/${twinId}/assets/batch-get`, {
605
+ headers: { 'Content-Type': 'application/json' },
606
+ body: JSON.stringify({ ids }),
607
+ signal
608
+ });
609
+ if (response.ok) {
610
+ return (await response.json());
611
+ }
612
+ if (response.status === 400) {
613
+ const errorBody = await readErrorBody(response);
614
+ throw new AssetValidationError(`Asset batch-get validation failed: ${errorBody.detail}`, errorBody);
615
+ }
616
+ const errorText = await response.text();
617
+ throw new Error(`Failed to batch-get assets: ${response.status} ${response.statusText} - ${errorText}`);
618
+ }
619
+ // ────────── Asset attachments ──────────
620
+ async listAssetAttachments(twinId, assetId, options, signal) {
621
+ const params = new URLSearchParams();
622
+ if (options?.includeDeleted)
623
+ params.set('includeDeleted', 'true');
624
+ if (options?.offset !== undefined)
625
+ params.set('offset', String(options.offset));
626
+ if (options?.limit !== undefined)
627
+ params.set('limit', String(options.limit));
628
+ if (options?.page)
629
+ params.set('page', options.page);
630
+ const url = `${this._baseUrl}/${twinId}/assets/${assetId}/attachments${params.toString() ? `?${params}` : ''}`;
631
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
632
+ headers: { Accept: 'application/json' },
633
+ signal
634
+ });
635
+ if (response.ok)
636
+ return (await response.json());
637
+ if (response.status === 404)
638
+ throw new AssetNotFoundError(`Asset ${assetId} not found`);
639
+ if (response.status === 400) {
640
+ const errorBody = await readErrorBody(response);
641
+ throw new AssetValidationError(`Attachment listing failed: ${errorBody.detail}`, errorBody);
642
+ }
643
+ const errorText = await response.text();
644
+ throw new Error(`Failed to list attachments: ${response.status} ${response.statusText} - ${errorText}`);
645
+ }
646
+ async getAssetAttachmentBlob(twinId, assetId, attachmentId, signal) {
647
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/assets/${assetId}/attachments/${attachmentId}`, { signal });
648
+ if (response.ok)
649
+ return response;
650
+ if (response.status === 404)
651
+ throw new AssetAttachmentNotFoundError(`Attachment ${attachmentId} not found`);
652
+ const errorText = await response.text();
653
+ throw new Error(`Failed to fetch attachment blob: ${response.status} ${response.statusText} - ${errorText}`);
654
+ }
655
+ async createAssetAttachmentUploadSession(twinId, assetId, attachmentId, body, signal) {
656
+ const response = await this._httpClient.fetch(HttpMethod.Post, `${this._baseUrl}/${twinId}/assets/${assetId}/attachments/${attachmentId}/createAttachmentUploadSession`, {
657
+ headers: { 'Content-Type': 'application/json' },
658
+ body: JSON.stringify(body),
659
+ signal
660
+ });
661
+ if (response.ok)
662
+ return (await response.json());
663
+ if (response.status === 409) {
664
+ const conflicted = (await response.json());
665
+ throw new AssetAttachmentConflictError(`Attachment ${attachmentId} already exists; pick a new id for retries`, conflicted);
666
+ }
667
+ if (response.status === 404)
668
+ throw new AssetNotFoundError(`Asset ${assetId} not found`);
669
+ if (response.status === 400) {
670
+ const errorBody = await readErrorBody(response);
671
+ throw new AssetValidationError(`Upload session creation failed: ${errorBody.detail}`, errorBody);
672
+ }
673
+ const errorText = await response.text();
674
+ throw new Error(`Failed to create upload session: ${response.status} ${response.statusText} - ${errorText}`);
675
+ }
676
+ async uploadAssetAttachmentFile(session, file, signal) {
677
+ // The token is encoded as a query param on the upload URL; we lift it
678
+ // out for the caller so they can poll uploadStatus without re-parsing.
679
+ const tokenMatch = session.uploadUrl.match(/token=([^&]+)/);
680
+ if (!tokenMatch) {
681
+ throw new Error(`Upload session URL is missing a token query parameter: ${session.uploadUrl}`);
682
+ }
683
+ const token = tokenMatch[1];
684
+ const chunkSize = Math.max(1, session.maxChunkSize || file.size);
685
+ const total = file.size;
686
+ let offset = 0;
687
+ // Even an empty file needs at least one PUT so the upload session
688
+ // gets a finalize signal. uploadChunks() in the integration tests
689
+ // unconditionally PUTs at least one chunk too.
690
+ do {
691
+ const end = Math.min(offset + chunkSize, total);
692
+ const chunk = file.slice(offset, end);
693
+ const lastByte = Math.max(0, end - 1);
694
+ const response = await this._httpClient.fetch(HttpMethod.Put, session.uploadUrl, {
695
+ headers: { 'Content-Range': `bytes ${offset}-${lastByte}/${total}` },
696
+ body: chunk,
697
+ signal
698
+ });
699
+ if (!response.ok) {
700
+ const errorText = await response.text();
701
+ throw new Error(`Upload chunk failed at offset ${offset}: ${response.status} ${response.statusText} - ${errorText}`);
702
+ }
703
+ offset = end;
704
+ } while (offset < total);
705
+ return { token };
706
+ }
707
+ async getAssetAttachmentUploadStatus(twinId, assetId, token, signal) {
708
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/assets/${assetId}/attachments/uploadStatus/${encodeURIComponent(token)}`, { headers: { Accept: 'application/json' }, signal });
709
+ if (response.ok)
710
+ return (await response.json());
711
+ if (response.status === 404)
712
+ return { status: 'NotFound' };
713
+ const errorText = await response.text();
714
+ throw new Error(`Failed to get upload status: ${response.status} ${response.statusText} - ${errorText}`);
715
+ }
716
+ async putAssetAttachment(twinId, assetId, attachmentId, body, signal) {
717
+ const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}/assets/${assetId}/attachments/${attachmentId}`, {
718
+ headers: { 'Content-Type': 'application/json' },
719
+ body: JSON.stringify(body),
720
+ signal
721
+ });
722
+ if (response.ok)
723
+ return (await response.json());
724
+ if (response.status === 409) {
725
+ const conflicted = (await response.json());
726
+ throw new AssetAttachmentConflictError('Attachment has been modified by another user', conflicted);
727
+ }
728
+ if (response.status === 404)
729
+ throw new AssetAttachmentNotFoundError(`Attachment ${attachmentId} not found`);
730
+ if (response.status === 400) {
731
+ const errorBody = await readErrorBody(response);
732
+ throw new AssetValidationError(`Attachment update failed: ${errorBody.detail}`, errorBody);
733
+ }
734
+ const errorText = await response.text();
735
+ throw new Error(`Failed to update attachment: ${response.status} ${response.statusText} - ${errorText}`);
736
+ }
737
+ async deleteAssetAttachment(twinId, assetId, attachmentId, etag, signal) {
738
+ const response = await this._httpClient.fetch(HttpMethod.Delete, `${this._baseUrl}/${twinId}/assets/${assetId}/attachments/${attachmentId}`, {
739
+ headers: { 'If-Match': etag },
740
+ signal
741
+ });
742
+ if (response.ok)
743
+ return (await response.json());
744
+ if (response.status === 409) {
745
+ const conflicted = (await response.json());
746
+ throw new AssetAttachmentConflictError('Attachment has been modified by another user', conflicted);
747
+ }
748
+ if (response.status === 404)
749
+ throw new AssetAttachmentNotFoundError(`Attachment ${attachmentId} not found`);
750
+ const errorText = await response.text();
751
+ throw new Error(`Failed to delete attachment: ${response.status} ${response.statusText} - ${errorText}`);
752
+ }
753
+ async restoreAssetAttachment(twinId, assetId, attachmentId, etag, signal) {
754
+ const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}/assets/${assetId}/attachments/${attachmentId}/restore`, {
755
+ headers: { 'Content-Type': 'application/json' },
756
+ body: JSON.stringify({ etag }),
757
+ signal
758
+ });
759
+ if (response.ok)
760
+ return (await response.json());
761
+ if (response.status === 409) {
762
+ const conflicted = (await response.json());
763
+ throw new AssetAttachmentConflictError('Attachment has been modified by another user', conflicted);
764
+ }
765
+ if (response.status === 404)
766
+ throw new AssetAttachmentNotFoundError(`Attachment ${attachmentId} not found`);
767
+ const errorText = await response.text();
768
+ throw new Error(`Failed to restore attachment: ${response.status} ${response.statusText} - ${errorText}`);
769
+ }
770
+ async listTwinFiles(twinId, options = {}, signal) {
771
+ const { includeDeleted = false, checkedIn = false, version, page = undefined, limit = 100 } = options;
772
+ if (version !== undefined && !checkedIn) {
773
+ throw new Error('The "version" option can only be used when "checkedIn" is true.');
774
+ }
775
+ const params = new URLSearchParams();
776
+ if (includeDeleted)
777
+ params.set('includeDeleted', 'true');
778
+ if (checkedIn)
779
+ params.set('checkedIn', 'true');
780
+ if (version !== undefined)
781
+ params.set('version', version.toString());
782
+ if (page !== undefined)
783
+ params.set('page', page);
784
+ if (limit !== 100)
785
+ params.set('limit', limit.toString());
786
+ const url = params.toString()
787
+ ? `${this._baseUrl}/${twinId}/files?${params}`
788
+ : `${this._baseUrl}/${twinId}/files`;
789
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
790
+ headers: { Accept: 'application/json' },
791
+ signal
792
+ });
793
+ if (response.ok) {
794
+ return (await response.json());
795
+ }
796
+ if (response.status === 400) {
797
+ const errorBody = await readErrorBody(response);
798
+ throw new TwinFileValidationError(`Twin file listing validation failed: ${errorBody.detail}`, errorBody);
799
+ }
800
+ const errorText = await response.text();
801
+ throw new Error(`Failed to list twin files: ${response.status} ${response.statusText} - ${errorText}`);
802
+ }
803
+ async getTwinFile(twinId, fileId, signal) {
804
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/files/${fileId}`, {
805
+ headers: { Accept: 'application/json' },
806
+ signal
807
+ });
808
+ if (response.ok) {
809
+ return (await response.json());
810
+ }
811
+ if (response.status === 404) {
812
+ throw new TwinFileNotFoundError(`Twin file ${fileId} not found`);
813
+ }
814
+ if (response.status === 400) {
815
+ const errorText = await response.text();
816
+ throw new TwinFileValidationError(`Twin file validation failed: ${errorText}`);
817
+ }
818
+ const errorText = await response.text();
819
+ throw new Error(`Failed to get twin file: ${response.status} ${response.statusText} - ${errorText}`);
820
+ }
821
+ async createOrUpdateTwinFile(twinId, fileId, twinFile, signal) {
822
+ const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}/files/${fileId}`, {
823
+ headers: { 'Content-Type': 'application/json' },
824
+ body: JSON.stringify(twinFile),
825
+ signal
826
+ });
827
+ if (response.ok || response.status === 201) {
828
+ return (await response.json());
829
+ }
830
+ if (response.status === 400) {
831
+ const errorBody = await readErrorBody(response);
832
+ throw new TwinFileValidationError(`Twin file validation failed`, errorBody);
833
+ }
834
+ if (response.status === 409) {
835
+ const conflictedFile = (await response.json());
836
+ throw new TwinFileConflictError('Twin file has been modified by another user', conflictedFile);
837
+ }
838
+ const errorText = await response.text();
839
+ throw new Error(`Failed to create/update twin file: ${response.status} ${response.statusText} - ${errorText}`);
840
+ }
841
+ async deleteTwinFile(twinId, fileId, deleteRequest, signal) {
842
+ const response = await this._httpClient.fetch(HttpMethod.Delete, `${this._baseUrl}/${twinId}/files/${fileId}`, {
843
+ headers: { 'Content-Type': 'application/json' },
844
+ body: JSON.stringify(deleteRequest),
845
+ signal
846
+ });
847
+ if (response.ok) {
848
+ return (await response.json());
849
+ }
850
+ if (response.status === 404) {
851
+ throw new TwinFileNotFoundError(`Twin file ${fileId} not found`);
852
+ }
853
+ if (response.status === 409) {
854
+ const conflictedFile = (await response.json());
855
+ throw new TwinFileConflictError('Twin file has been modified by another user', conflictedFile);
856
+ }
857
+ if (response.status === 400) {
858
+ const errorText = await response.text();
859
+ throw new TwinFileValidationError(`Twin file validation failed: ${errorText}`);
860
+ }
861
+ const errorText = await response.text();
862
+ throw new Error(`Failed to delete twin file: ${response.status} ${response.statusText} - ${errorText}`);
863
+ }
864
+ async restoreTwinFile(twinId, fileId, etag, signal) {
865
+ const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}/files/${fileId}/restore`, {
866
+ headers: { 'Content-Type': 'application/json' },
867
+ body: JSON.stringify({ etag }),
868
+ signal
869
+ });
870
+ if (response.ok) {
871
+ return (await response.json());
872
+ }
873
+ if (response.status === 404) {
874
+ throw new TwinFileNotFoundError(`Twin file ${fileId} not found`);
875
+ }
876
+ if (response.status === 409) {
877
+ const conflictedFile = (await response.json());
878
+ throw new TwinFileConflictError('Twin file has been modified by another user', conflictedFile);
879
+ }
880
+ if (response.status === 400) {
881
+ const errorText = await response.text();
882
+ throw new TwinFileValidationError(`Twin file validation failed: ${errorText}`);
883
+ }
884
+ const errorText = await response.text();
885
+ throw new Error(`Failed to restore twin file: ${response.status} ${response.statusText} - ${errorText}`);
886
+ }
887
+ async getTwinFileContent(twinId, fileId, signal) {
888
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/files/${fileId}/content`, { signal });
889
+ if (response.ok) {
890
+ return await response.blob();
891
+ }
892
+ if (response.status === 404) {
893
+ throw new TwinFileNotFoundError(`Twin file ${fileId} not found`);
894
+ }
895
+ const errorText = await response.text();
896
+ throw new Error(`Failed to get twin file content: ${response.status} ${response.statusText} - ${errorText}`);
897
+ }
898
+ async getTwinFileVersion(twinId, fileId, version, signal) {
899
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/files/${fileId}/versions/${version}`, { signal });
900
+ if (response.ok) {
901
+ return (await response.json());
902
+ }
903
+ if (response.status === 404) {
904
+ throw new TwinFileNotFoundError(`Twin file ${fileId} version ${version} not found`);
905
+ }
906
+ const errorText = await response.text();
907
+ throw new Error(`Failed to get twin file version: ${response.status} ${response.statusText} - ${errorText}`);
908
+ }
909
+ async listTwinFileVersions(twinId, fileId, options, signal) {
910
+ const params = new URLSearchParams();
911
+ if (options?.includeDeleted !== undefined)
912
+ params.append('includeDeleted', String(options.includeDeleted));
913
+ if (options?.offset !== undefined)
914
+ params.append('offset', String(options.offset));
915
+ if (options?.limit !== undefined)
916
+ params.append('limit', String(options.limit));
917
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/files/${fileId}/versions?${params.toString()}`, { signal });
918
+ if (response.ok) {
919
+ return (await response.json());
920
+ }
921
+ if (response.status === 400) {
922
+ const errorBody = await readErrorBody(response);
923
+ throw new TwinFileValidationError(`Twin file version listing validation failed: ${errorBody.detail}`, errorBody);
924
+ }
925
+ const errorText = await response.text();
926
+ throw new Error(`Failed to list twin file versions: ${response.status} ${response.statusText} - ${errorText}`);
927
+ }
928
+ async getTwinFileVersionContent(twinId, fileId, version, signal) {
929
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/files/${fileId}/versions/${version}/content`, { signal });
930
+ if (response.ok) {
931
+ return await response.blob();
932
+ }
933
+ if (response.status === 404) {
934
+ throw new TwinFileNotFoundError(`Twin file ${fileId} version ${version} content not found`);
935
+ }
936
+ const errorText = await response.text();
937
+ throw new Error(`Failed to get twin file version content: ${response.status} ${response.statusText} - ${errorText}`);
938
+ }
939
+ async createOrUpdateTwinRaw(twinId, twin, signal) {
940
+ return await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}`, {
941
+ headers: { 'Content-Type': 'application/json' },
942
+ body: JSON.stringify(twin),
943
+ signal
944
+ });
945
+ }
946
+ async createOrUpdateTwinFileRaw(twinId, fileId, twinFile, signal) {
947
+ return await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}/files/${fileId}`, {
948
+ headers: { 'Content-Type': 'application/json' },
949
+ body: JSON.stringify(twinFile),
950
+ signal
951
+ });
952
+ }
953
+ // ---------- Asset categories ----------
954
+ async getMyPermissions(signal) {
955
+ const url = `${this._baseUrl}/permissions/me`;
956
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
957
+ headers: { Accept: 'application/json' },
958
+ signal
959
+ });
960
+ if (response.ok) {
961
+ return (await response.json());
962
+ }
963
+ // 401: no valid auth context. Throw so consumers can redirect to
964
+ // login / refresh tokens rather than mistake an expired session for
965
+ // "logged-in user with no grants."
966
+ if (response.status === 401) {
967
+ throw new TwinUnauthenticatedError('Not authenticated');
968
+ }
969
+ // 403: authenticated but no Twin-service capabilities. Surface as an
970
+ // empty array so consumers can treat it like a logged-in user with
971
+ // zero grants.
972
+ if (response.status === 403) {
973
+ return { permissions: [] };
974
+ }
975
+ throw new Error(`Failed to fetch permissions: ${response.status} ${response.statusText}`);
976
+ }
977
+ // ---------- Twin-scoped permissions (TWINS-2982) ----------
978
+ async getMyTwinPermissions(twinId, signal) {
979
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/permissions/me`, {
980
+ headers: { Accept: 'application/json' },
981
+ signal
982
+ });
983
+ if (response.ok)
984
+ return (await response.json());
985
+ return this._failTwinPermissionsRequest(response, 'read the capabilities on the twin', twinId);
986
+ }
987
+ async listTwinPermissions(twinId, options, signal) {
988
+ const params = new URLSearchParams();
989
+ if (options?.page !== undefined)
990
+ params.set('page', options.page);
991
+ if (options?.limit !== undefined)
992
+ params.set('limit', options.limit.toString());
993
+ const query = params.toString();
994
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/permissions${query ? `?${query}` : ''}`, { headers: { Accept: 'application/json' }, signal });
995
+ if (response.ok)
996
+ return (await response.json());
997
+ return this._failTwinPermissionsRequest(response, 'list the twin permissions', twinId);
998
+ }
999
+ async listAllTwinPermissions(twinId, signal) {
1000
+ const limit = 1000;
1001
+ const entries = [];
1002
+ // Paged on a short page rather than on `nextPage`, which the service emits even for the
1003
+ // last page — the same reason `listAllTwins` pages this way.
1004
+ let page;
1005
+ let received;
1006
+ do {
1007
+ const response = await this.listTwinPermissions(twinId, { page, limit }, signal);
1008
+ entries.push(...response.data);
1009
+ received = response.data.length;
1010
+ page = response.pagination.nextPage;
1011
+ } while (received >= limit);
1012
+ return entries;
1013
+ }
1014
+ async setTwinPermissions(twinId, principalId, body, signal) {
1015
+ const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/${twinId}/permissions/${principalId}`, {
1016
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
1017
+ body: JSON.stringify(body),
1018
+ signal
1019
+ });
1020
+ if (response.ok)
1021
+ return (await response.json());
1022
+ return this._failTwinPermissionsRequest(response, 'set the twin permissions', twinId, {
1023
+ id: principalId,
1024
+ label: 'Principal'
1025
+ });
1026
+ }
1027
+ async deleteTwinPermissions(twinId, principalId, etag, signal) {
1028
+ const response = await this._httpClient.fetch(HttpMethod.Delete, `${this._baseUrl}/${twinId}/permissions/${principalId}`, {
1029
+ // The etag travels in the header, which is the transport the surface prefers and
1030
+ // the one that wins when both are present. It is repeated in the body — the other
1031
+ // carrier ADR-0008 documents, and the one the endpoint binds — so that the request
1032
+ // still names an etag if a proxy strips the header, or drops the body of a DELETE.
1033
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'If-Match': etag },
1034
+ body: JSON.stringify({ etag }),
1035
+ signal
1036
+ });
1037
+ if (response.ok)
1038
+ return (await response.json());
1039
+ return this._failTwinPermissionsRequest(response, 'revoke the twin permissions', twinId, {
1040
+ id: principalId,
1041
+ label: 'Principal'
1042
+ });
1043
+ }
1044
+ async listTwinGroups(twinId, signal) {
1045
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/groups`, {
1046
+ headers: { Accept: 'application/json' },
1047
+ signal
1048
+ });
1049
+ if (response.ok)
1050
+ return (await response.json());
1051
+ return this._failTwinPermissionsRequest(response, "list the twin's groups", twinId);
1052
+ }
1053
+ async getTwinGroup(twinId, groupId, signal) {
1054
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/${twinId}/groups/${groupId}`, {
1055
+ headers: { Accept: 'application/json' },
1056
+ signal
1057
+ });
1058
+ if (response.ok)
1059
+ return (await response.json());
1060
+ return this._failTwinPermissionsRequest(response, "get the twin's group", twinId, {
1061
+ id: groupId,
1062
+ label: 'Group'
1063
+ });
1064
+ }
1065
+ // ---------- Asset-domain permissions ----------
1066
+ async listAssetPermissions(options, signal) {
1067
+ const params = new URLSearchParams();
1068
+ if (options?.page !== undefined)
1069
+ params.set('page', options.page);
1070
+ if (options?.limit !== undefined)
1071
+ params.set('limit', options.limit.toString());
1072
+ const query = params.toString();
1073
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/permissions${query ? `?${query}` : ''}`, { headers: { Accept: 'application/json' }, signal });
1074
+ if (response.ok)
1075
+ return (await response.json());
1076
+ return this._failAssetPermissionsRequest(response, 'list the asset permissions');
1077
+ }
1078
+ async listAllAssetPermissions(signal) {
1079
+ const limit = 1000;
1080
+ const entries = [];
1081
+ // Paged on a short page rather than on `nextPage`, which the service emits even for the
1082
+ // last page — the same reason `listAllTwinPermissions` pages this way.
1083
+ let page;
1084
+ let received;
1085
+ do {
1086
+ const response = await this.listAssetPermissions({ page, limit }, signal);
1087
+ entries.push(...response.data);
1088
+ received = response.data.length;
1089
+ page = response.pagination.nextPage;
1090
+ } while (received >= limit);
1091
+ return entries;
1092
+ }
1093
+ async setAssetPermissions(principalId, body, signal) {
1094
+ const response = await this._httpClient.fetch(HttpMethod.Put, `${this._baseUrl}/permissions/${principalId}`, {
1095
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
1096
+ body: JSON.stringify(body),
1097
+ signal
1098
+ });
1099
+ if (response.ok)
1100
+ return (await response.json());
1101
+ return this._failAssetPermissionsRequest(response, 'set the asset permissions', {
1102
+ id: principalId,
1103
+ label: 'Principal'
1104
+ });
1105
+ }
1106
+ async deleteAssetPermissions(principalId, etag, signal) {
1107
+ const response = await this._httpClient.fetch(HttpMethod.Delete, `${this._baseUrl}/permissions/${principalId}`, {
1108
+ // The etag travels in the header, which is the transport the surface prefers and the
1109
+ // one that wins when both are present. It is repeated in the body — the other
1110
+ // carrier the endpoint binds — so the request still names an etag if a proxy strips
1111
+ // the header, or drops the body of a DELETE.
1112
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'If-Match': etag },
1113
+ body: JSON.stringify({ etag }),
1114
+ signal
1115
+ });
1116
+ if (response.ok)
1117
+ return (await response.json());
1118
+ return this._failAssetPermissionsRequest(response, 'revoke the asset permissions', {
1119
+ id: principalId,
1120
+ label: 'Principal'
1121
+ });
1122
+ }
1123
+ async listAssetGroups(signal) {
1124
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/groups`, {
1125
+ headers: { Accept: 'application/json' },
1126
+ signal
1127
+ });
1128
+ if (response.ok)
1129
+ return (await response.json());
1130
+ return this._failAssetPermissionsRequest(response, "list the asset domain's default groups");
1131
+ }
1132
+ async getAssetGroup(name, signal) {
1133
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._baseUrl}/groups/${encodeURIComponent(name)}`, { headers: { Accept: 'application/json' }, signal });
1134
+ if (response.ok)
1135
+ return (await response.json());
1136
+ return this._failAssetPermissionsRequest(response, "get the asset domain's default group", {
1137
+ id: name,
1138
+ label: 'Group'
1139
+ });
1140
+ }
1141
+ /**
1142
+ * Maps a refusal from the customer-wide asset permission surface onto the typed error its
1143
+ * status and cause name. The twin-scoped counterpart of this cannot be reused: its 404s are
1144
+ * about a twin, and this surface has none — a 404 naming no principal means the *name* was
1145
+ * unknown, not that a resource was missing.
1146
+ *
1147
+ * A 404 is dispatched on `item.kind`, which is what separates "that group has not been seeded
1148
+ * for this customer yet", which is worth retrying, from "no such group name", which is not.
1149
+ */
1150
+ async _failAssetPermissionsRequest(response, action, principal) {
1151
+ if (response.status === 401) {
1152
+ throw new TwinUnauthenticatedError('Not authenticated');
1153
+ }
1154
+ const errorBody = await readErrorBody(response);
1155
+ switch (response.status) {
1156
+ case 400:
1157
+ throw new TwinValidationError(`Failed to ${action}: ${errorBody.detail}`, errorBody);
1158
+ case 403:
1159
+ // The message is the server's own, never a reason invented here: a Twin service that
1160
+ // could not evaluate the request answers a bodiless 403, and `errorBody.cause` is
1161
+ // what separates that from a genuine `InsufficientPermissions`.
1162
+ throw new TwinPermissionsForbiddenError(`Failed to ${action}: ${errorBody.detail}`, errorBody);
1163
+ case 404:
1164
+ if (errorBody.item?.kind === 'Principal' && principal !== undefined) {
1165
+ throw new TwinPrincipalNotFoundError(`${principal.label} ${principal.id} not found on the asset domain`, errorBody);
1166
+ }
1167
+ throw new TwinNotFoundError(`Failed to ${action}: ${errorBody.detail}`, errorBody);
1168
+ case 409:
1169
+ throw new AssetPermissionsConflictError(`Failed to ${action}: ${errorBody.detail}`, errorBody);
1170
+ case 503:
1171
+ throw new TwinTemporarilyUnavailableError(`Failed to ${action}: ${errorBody.detail}`, errorBody, retryAfterSeconds(response));
1172
+ }
1173
+ // A 500 can also mean the Permission service failed: the Twin service answers 503 when the
1174
+ // call threw, but 500 when it returned an upstream failure. Only the 503 half is classified
1175
+ // as retryable here, because the 500 arrives as a ProblemDetails payload with no cause to
1176
+ // recognise it by.
1177
+ throw new Error(`Failed to ${action}: ${response.status} ${response.statusText} - ${errorBody.detail}`);
1178
+ }
1179
+ /**
1180
+ * Maps a refusal from the twin-scoped permission surface onto the typed error its status
1181
+ * and cause name. Shared by all six methods because the surface answers them uniformly:
1182
+ * the status is decided by the request's shape and the caller's grants, not by the route.
1183
+ *
1184
+ * A 404 is dispatched on `item.kind`, which is the only thing separating "no such twin, or
1185
+ * none you may see" from "the principal you named is not there" — the two arrive on the
1186
+ * same status from the same route.
1187
+ */
1188
+ async _failTwinPermissionsRequest(response, action, twinId, principal) {
1189
+ if (response.status === 401) {
1190
+ throw new TwinUnauthenticatedError('Not authenticated');
1191
+ }
1192
+ const errorBody = await readErrorBody(response);
1193
+ switch (response.status) {
1194
+ case 400:
1195
+ throw new TwinValidationError(`Failed to ${action}: ${errorBody.detail}`, errorBody);
1196
+ case 403:
1197
+ // The message is the server's own, never a reason invented here: a 403 does not
1198
+ // always mean "you lack ManageTwinPermissions". `/permissions/me` has no capability
1199
+ // gate at all, and a Twin service that could not evaluate the request answers a
1200
+ // bodiless 403 — `errorBody.cause` is what separates those, so it travels with it.
1201
+ throw new TwinPermissionsForbiddenError(`Failed to ${action}: ${errorBody.detail}`, errorBody);
1202
+ case 404:
1203
+ if (errorBody.item?.kind === 'Principal' && principal !== undefined) {
1204
+ throw new TwinPrincipalNotFoundError(`${principal.label} ${principal.id} not found on twin ${twinId}`, errorBody);
1205
+ }
1206
+ throw new TwinNotFoundError(`Twin ${twinId} not found`, errorBody);
1207
+ case 409:
1208
+ throw new TwinPermissionsConflictError(`Failed to ${action}: ${errorBody.detail}`, errorBody);
1209
+ case 503:
1210
+ throw new TwinTemporarilyUnavailableError(`Failed to ${action}: ${errorBody.detail}`, errorBody, retryAfterSeconds(response));
1211
+ }
1212
+ // Note that a 500 can also mean the Permission service failed: the Twin service answers 503
1213
+ // when the call threw, but 500 when it returned an upstream failure. Only the 503 half is
1214
+ // classified as retryable here, because the 500 arrives as a ProblemDetails payload with no
1215
+ // cause to recognise it by.
1216
+ throw new Error(`Failed to ${action}: ${response.status} ${response.statusText} - ${errorBody.detail}`);
1217
+ }
1218
+ async getAssetTypeMetaSchema(signal) {
1219
+ const url = `${this._assetTypeCatalogUrl}/meta-schema`;
1220
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
1221
+ headers: { Accept: 'application/schema+json, application/json' },
1222
+ signal
1223
+ });
1224
+ if (!response.ok) {
1225
+ throw new Error(`Failed to fetch meta-schema: ${response.status} ${response.statusText}`);
1226
+ }
1227
+ return (await response.json());
1228
+ }
1229
+ async listAssetCategories(options, signal) {
1230
+ const params = new URLSearchParams();
1231
+ if (options?.page !== undefined)
1232
+ params.set('page', options.page);
1233
+ if (options?.limit !== undefined)
1234
+ params.set('limit', options.limit.toString());
1235
+ if (options?.includeDeleted)
1236
+ params.set('includeDeleted', 'true');
1237
+ const url = params.toString()
1238
+ ? `${this._assetTypeCatalogUrl}/categories?${params}`
1239
+ : `${this._assetTypeCatalogUrl}/categories`;
1240
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
1241
+ headers: { Accept: 'application/json' },
1242
+ signal
1243
+ });
1244
+ if (response.ok)
1245
+ return (await response.json());
1246
+ if (response.status === 403)
1247
+ throw new AssetCategoryForbiddenError('Asset category listing forbidden');
1248
+ if (response.status === 400) {
1249
+ const errorBody = await readErrorBody(response);
1250
+ throw new AssetCategoryValidationError(`Asset category listing failed: ${errorBody.detail}`, errorBody);
1251
+ }
1252
+ throw new Error(`Failed to list asset categories: ${response.status} ${response.statusText}`);
1253
+ }
1254
+ async getAssetCategory(categoryId, signal) {
1255
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._assetTypeCatalogUrl}/categories/${categoryId}`, {
1256
+ headers: { Accept: 'application/json' },
1257
+ signal
1258
+ });
1259
+ if (response.ok)
1260
+ return (await response.json());
1261
+ if (response.status === 403)
1262
+ throw new AssetCategoryForbiddenError(`Asset category ${categoryId} read forbidden`);
1263
+ if (response.status === 404)
1264
+ throw new AssetCategoryNotFoundError(`Asset category ${categoryId} not found`);
1265
+ throw new Error(`Failed to get asset category: ${response.status} ${response.statusText}`);
1266
+ }
1267
+ async putAssetCategory(categoryId, body, signal) {
1268
+ const response = await this._httpClient.fetch(HttpMethod.Put, `${this._assetTypeCatalogUrl}/categories/${categoryId}`, {
1269
+ headers: { 'Content-Type': 'application/json' },
1270
+ body: JSON.stringify(body),
1271
+ signal
1272
+ });
1273
+ if (response.ok)
1274
+ return (await response.json());
1275
+ if (response.status === 403)
1276
+ throw new AssetCategoryForbiddenError('Asset category upsert forbidden');
1277
+ if (response.status === 409) {
1278
+ const current = (await response.json());
1279
+ throw new AssetCategoryConflictError('Asset category was modified by another user', current);
1280
+ }
1281
+ if (response.status === 400) {
1282
+ const errorBody = await readErrorBody(response);
1283
+ throw new AssetCategoryValidationError(`Asset category validation failed: ${errorBody.detail}`, errorBody);
1284
+ }
1285
+ const errorText = await response.text();
1286
+ throw new Error(`Failed to upsert asset category: ${response.status} ${response.statusText} - ${errorText}`);
1287
+ }
1288
+ async deleteAssetCategory(categoryId, etag, signal) {
1289
+ return this._categoryDeleteOrRestore(categoryId, etag, /*isRestore*/ false, signal);
1290
+ }
1291
+ async restoreAssetCategory(categoryId, etag, signal) {
1292
+ return this._categoryDeleteOrRestore(categoryId, etag, /*isRestore*/ true, signal);
1293
+ }
1294
+ async _categoryDeleteOrRestore(categoryId, etag, isRestore, signal) {
1295
+ const url = isRestore
1296
+ ? `${this._assetTypeCatalogUrl}/categories/${categoryId}/restore`
1297
+ : `${this._assetTypeCatalogUrl}/categories/${categoryId}`;
1298
+ const method = isRestore ? HttpMethod.Put : HttpMethod.Delete;
1299
+ const response = await this._httpClient.fetch(method, url, {
1300
+ headers: { 'Content-Type': 'application/json' },
1301
+ body: JSON.stringify({ etag }),
1302
+ signal
1303
+ });
1304
+ if (response.ok)
1305
+ return (await response.json());
1306
+ if (response.status === 403) {
1307
+ const op = isRestore ? 'restore' : 'delete';
1308
+ throw new AssetCategoryForbiddenError(`Asset category ${op} forbidden`);
1309
+ }
1310
+ if (response.status === 409) {
1311
+ const current = (await response.json());
1312
+ throw new AssetCategoryConflictError('Asset category was modified by another user', current);
1313
+ }
1314
+ if (response.status === 404)
1315
+ throw new AssetCategoryNotFoundError(`Asset category ${categoryId} not found`);
1316
+ const errorText = await response.text();
1317
+ const op = isRestore ? 'restore' : 'delete';
1318
+ throw new Error(`Failed to ${op} asset category: ${response.status} ${response.statusText} - ${errorText}`);
1319
+ }
1320
+ async listAssetCategoryVersions(categoryId, options, signal) {
1321
+ const params = new URLSearchParams();
1322
+ if (options?.page !== undefined)
1323
+ params.set('page', options.page);
1324
+ if (options?.limit !== undefined)
1325
+ params.set('limit', options.limit.toString());
1326
+ if (options?.includeDeleted)
1327
+ params.set('includeDeleted', 'true');
1328
+ const baseUrl = `${this._assetTypeCatalogUrl}/categories/${categoryId}/versions`;
1329
+ const url = params.toString() ? `${baseUrl}?${params}` : baseUrl;
1330
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
1331
+ headers: { Accept: 'application/json' },
1332
+ signal
1333
+ });
1334
+ if (response.ok)
1335
+ return (await response.json());
1336
+ if (response.status === 403)
1337
+ throw new AssetCategoryForbiddenError('Asset category version listing forbidden');
1338
+ if (response.status === 400) {
1339
+ const errorBody = await readErrorBody(response);
1340
+ throw new AssetCategoryValidationError(`Asset category version listing failed: ${errorBody.detail}`, errorBody);
1341
+ }
1342
+ if (response.status === 404)
1343
+ throw new AssetCategoryNotFoundError(`Asset category ${categoryId} not found`);
1344
+ throw new Error(`Failed to list asset category versions: ${response.status} ${response.statusText}`);
1345
+ }
1346
+ // ---------- Asset types ----------
1347
+ async listAssetTypes(categoryId, options, signal) {
1348
+ const params = new URLSearchParams();
1349
+ if (options?.page !== undefined)
1350
+ params.set('page', options.page);
1351
+ if (options?.limit !== undefined)
1352
+ params.set('limit', options.limit.toString());
1353
+ if (options?.includeDeleted)
1354
+ params.set('includeDeleted', 'true');
1355
+ const baseUrl = `${this._assetTypeCatalogUrl}/categories/${categoryId}/types`;
1356
+ const url = params.toString() ? `${baseUrl}?${params}` : baseUrl;
1357
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
1358
+ headers: { Accept: 'application/json' },
1359
+ signal
1360
+ });
1361
+ if (response.ok)
1362
+ return (await response.json());
1363
+ if (response.status === 403)
1364
+ throw new AssetTypeForbiddenError('Asset type listing forbidden');
1365
+ if (response.status === 400) {
1366
+ const errorBody = await readErrorBody(response);
1367
+ throw new AssetTypeValidationError(`Asset type listing failed: ${errorBody.detail}`, errorBody);
1368
+ }
1369
+ throw new Error(`Failed to list asset types: ${response.status} ${response.statusText}`);
1370
+ }
1371
+ async getAssetType(categoryId, typeId, signal) {
1372
+ const response = await this._httpClient.fetch(HttpMethod.Get, `${this._assetTypeCatalogUrl}/categories/${categoryId}/types/${typeId}`, { headers: { Accept: 'application/json' }, signal });
1373
+ if (response.ok)
1374
+ return (await response.json());
1375
+ if (response.status === 403)
1376
+ throw new AssetTypeForbiddenError(`Asset type ${typeId} read forbidden`);
1377
+ if (response.status === 404)
1378
+ throw new AssetTypeNotFoundError(`Asset type ${typeId} not found`);
1379
+ throw new Error(`Failed to get asset type: ${response.status} ${response.statusText}`);
1380
+ }
1381
+ async listAssetTypeVersions(categoryId, typeId, options, signal) {
1382
+ const params = new URLSearchParams();
1383
+ if (options?.page !== undefined)
1384
+ params.set('page', options.page);
1385
+ if (options?.limit !== undefined)
1386
+ params.set('limit', options.limit.toString());
1387
+ if (options?.includeDeleted)
1388
+ params.set('includeDeleted', 'true');
1389
+ const baseUrl = `${this._assetTypeCatalogUrl}/categories/${categoryId}/types/${typeId}/versions`;
1390
+ const url = params.toString() ? `${baseUrl}?${params}` : baseUrl;
1391
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
1392
+ headers: { Accept: 'application/json' },
1393
+ signal
1394
+ });
1395
+ if (response.ok)
1396
+ return (await response.json());
1397
+ if (response.status === 403)
1398
+ throw new AssetTypeForbiddenError('Asset type version listing forbidden');
1399
+ if (response.status === 400) {
1400
+ const errorBody = await readErrorBody(response);
1401
+ throw new AssetTypeValidationError(`Asset type version listing failed: ${errorBody.detail}`, errorBody);
1402
+ }
1403
+ if (response.status === 404)
1404
+ throw new AssetTypeNotFoundError(`Asset type ${typeId} not found`);
1405
+ throw new Error(`Failed to list asset type versions: ${response.status} ${response.statusText}`);
1406
+ }
1407
+ async putAssetType(categoryId, typeId, body, signal) {
1408
+ const response = await this._httpClient.fetch(HttpMethod.Put, `${this._assetTypeCatalogUrl}/categories/${categoryId}/types/${typeId}`, {
1409
+ headers: { 'Content-Type': 'application/json' },
1410
+ body: JSON.stringify(body),
1411
+ signal
1412
+ });
1413
+ if (response.ok)
1414
+ return (await response.json());
1415
+ if (response.status === 403)
1416
+ throw new AssetTypeForbiddenError('Asset type upsert forbidden');
1417
+ if (response.status === 409) {
1418
+ const current = (await response.json());
1419
+ throw new AssetTypeConflictError('Asset type was modified by another user', current);
1420
+ }
1421
+ if (response.status === 400) {
1422
+ const errorBody = await readErrorBody(response);
1423
+ throw new AssetTypeValidationError(`Asset type validation failed: ${errorBody.detail}`, errorBody);
1424
+ }
1425
+ const errorText = await response.text();
1426
+ throw new Error(`Failed to upsert asset type: ${response.status} ${response.statusText} - ${errorText}`);
1427
+ }
1428
+ async deleteAssetType(categoryId, typeId, etag, signal) {
1429
+ return this._typeDeleteOrRestore(categoryId, typeId, etag, /*isRestore*/ false, signal);
1430
+ }
1431
+ async restoreAssetType(categoryId, typeId, etag, signal) {
1432
+ return this._typeDeleteOrRestore(categoryId, typeId, etag, /*isRestore*/ true, signal);
1433
+ }
1434
+ async _typeDeleteOrRestore(categoryId, typeId, etag, isRestore, signal) {
1435
+ const url = isRestore
1436
+ ? `${this._assetTypeCatalogUrl}/categories/${categoryId}/types/${typeId}/restore`
1437
+ : `${this._assetTypeCatalogUrl}/categories/${categoryId}/types/${typeId}`;
1438
+ const method = isRestore ? HttpMethod.Put : HttpMethod.Delete;
1439
+ const response = await this._httpClient.fetch(method, url, {
1440
+ headers: { 'Content-Type': 'application/json' },
1441
+ body: JSON.stringify({ etag }),
1442
+ signal
1443
+ });
1444
+ if (response.ok)
1445
+ return (await response.json());
1446
+ if (response.status === 403) {
1447
+ const op = isRestore ? 'restore' : 'delete';
1448
+ throw new AssetTypeForbiddenError(`Asset type ${op} forbidden`);
1449
+ }
1450
+ if (response.status === 409) {
1451
+ const current = (await response.json());
1452
+ throw new AssetTypeConflictError('Asset type was modified by another user', current);
1453
+ }
1454
+ if (response.status === 404)
1455
+ throw new AssetTypeNotFoundError(`Asset type ${typeId} not found`);
1456
+ const errorText = await response.text();
1457
+ const op = isRestore ? 'restore' : 'delete';
1458
+ throw new Error(`Failed to ${op} asset type: ${response.status} ${response.statusText} - ${errorText}`);
1459
+ }
1460
+ async getAssetTypeCatalog(signal) {
1461
+ const response = await this._httpClient.fetch(HttpMethod.Get, this._assetTypeCatalogUrl, {
1462
+ headers: { Accept: 'application/json' },
1463
+ signal
1464
+ });
1465
+ if (response.ok)
1466
+ return (await response.json());
1467
+ if (response.status === 403)
1468
+ throw new AssetCategoryForbiddenError('Asset type catalog read forbidden');
1469
+ if (response.status === 400) {
1470
+ // The server emits a single structured 400 here today —
1471
+ // `cause: 'CatalogTooLarge'` — to flag a tenant whose catalog
1472
+ // overflows the per-customer cap. Surface as a typed error so
1473
+ // picker UIs can render "use search" instead of crashing on
1474
+ // missing rows. Any other 400 (malformed CatalogTooLarge body,
1475
+ // future cause) falls through to the generic error below so the
1476
+ // caller still sees the raw response.
1477
+ const errorBody = await readErrorBody(response);
1478
+ if (isAssetCatalogTooLargeErrorBody(errorBody)) {
1479
+ throw new AssetCatalogTooLargeError(`Asset type catalog too large: ${errorBody.detail}`, errorBody);
1480
+ }
1481
+ throw new Error(`Failed to get asset-type catalog: ${response.status} ${response.statusText} - ${errorBody.detail}`);
1482
+ }
1483
+ const errorText = await response.text();
1484
+ throw new Error(`Failed to get asset-type catalog: ${response.status} ${response.statusText} - ${errorText}`);
1485
+ }
1486
+ }
1487
+ //# sourceMappingURL=TwinClient.js.map