@twinfinity/twin 6.1.1 → 6.1.2-ci.31342
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -2
- package/dist/BuildInfo.js +3 -3
- package/dist/TwinClient.d.ts +312 -9
- package/dist/TwinClient.d.ts.map +1 -1
- package/dist/TwinClient.js +321 -6
- package/dist/TwinClient.js.map +1 -1
- package/dist/errors.d.ts +120 -5
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +116 -2
- package/dist/errors.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/generated.d.ts +95 -33
- package/dist/types/generated.d.ts.map +1 -1
- package/dist/types/twin-api.d.ts +54 -1
- package/dist/types/twin-api.d.ts.map +1 -1
- package/dist/types/twin-permissions-api.d.ts +226 -6
- package/dist/types/twin-permissions-api.d.ts.map +1 -1
- package/docs/assets/hierarchy.js +1 -1
- package/docs/assets/navigation.js +1 -1
- package/docs/assets/search.js +1 -1
- package/docs/classes/AssetAttachmentConflictError.html +1 -1
- package/docs/classes/AssetAttachmentNotFoundError.html +1 -1
- package/docs/classes/AssetBatchError.html +1 -1
- package/docs/classes/AssetCatalogTooLargeError.html +1 -1
- package/docs/classes/AssetCategoryConflictError.html +1 -1
- package/docs/classes/AssetCategoryForbiddenError.html +1 -1
- package/docs/classes/AssetCategoryNotFoundError.html +1 -1
- package/docs/classes/AssetCategoryValidationError.html +1 -1
- package/docs/classes/AssetConflictError.html +1 -1
- package/docs/classes/AssetForbiddenError.html +1 -1
- package/docs/classes/AssetNotFoundError.html +1 -1
- package/docs/classes/AssetPermissionsConflictError.html +16 -0
- package/docs/classes/AssetTypeConflictError.html +1 -1
- package/docs/classes/AssetTypeForbiddenError.html +1 -1
- package/docs/classes/AssetTypeNotFoundError.html +1 -1
- package/docs/classes/AssetTypeValidationError.html +1 -1
- package/docs/classes/AssetValidationError.html +1 -1
- package/docs/classes/ProcessorNotFoundError.html +1 -1
- package/docs/classes/TwinClient.html +251 -73
- package/docs/classes/TwinConflictError.html +1 -1
- package/docs/classes/TwinFileConflictError.html +1 -1
- package/docs/classes/TwinFileNotFoundError.html +1 -1
- package/docs/classes/TwinFileValidationError.html +1 -1
- package/docs/classes/TwinNotFoundError.html +4 -2
- package/docs/classes/TwinPermissionsConflictError.html +19 -0
- package/docs/classes/TwinPermissionsForbiddenError.html +12 -0
- package/docs/classes/TwinPrincipalNotFoundError.html +5 -0
- package/docs/classes/TwinTemporarilyUnavailableError.html +10 -0
- package/docs/classes/TwinUnauthenticatedError.html +1 -1
- package/docs/classes/TwinValidationError.html +1 -1
- package/docs/hierarchy.html +1 -1
- package/docs/index.html +56 -4
- package/docs/interfaces/AssetCatalogTooLargeErrorBody.html +2 -2
- package/docs/interfaces/AssetGroupGet.html +9 -0
- package/docs/interfaces/AssetGroupsGet.html +3 -0
- package/docs/interfaces/AssetPermissionsConflictErrorBody.html +12 -0
- package/docs/interfaces/AssetPermissionsPut.html +9 -0
- package/docs/interfaces/AssetPrincipalPermissionsGet.html +23 -0
- package/docs/interfaces/Coordinates.html +6 -0
- package/docs/interfaces/EntityCollection.html +7 -2
- package/docs/interfaces/EntityRepresentation.html +12 -0
- package/docs/interfaces/Facility.html +8 -3
- package/docs/interfaces/FacilityPart.html +5 -3
- package/docs/interfaces/ITwinClient.html +251 -73
- package/docs/interfaces/InheritedPermissions.html +8 -0
- package/docs/interfaces/ListTwinItem.html +18 -2
- package/docs/interfaces/Location.html +4 -1
- package/docs/interfaces/MyPermissions.html +2 -2
- package/docs/interfaces/MyTwinPermissions.html +5 -0
- package/docs/interfaces/Site.html +11 -4
- package/docs/interfaces/Space.html +6 -3
- package/docs/interfaces/TwinErrorBody.html +2 -2
- package/docs/interfaces/TwinGet.html +29 -11
- package/docs/interfaces/TwinGroupGet.html +10 -0
- package/docs/interfaces/TwinGroupsGet.html +3 -0
- package/docs/interfaces/TwinPermissionsConflictErrorBody.html +8 -0
- package/docs/interfaces/TwinPermissionsPut.html +11 -0
- package/docs/interfaces/TwinPrincipalPermissionsGet.html +34 -0
- package/docs/interfaces/TwinPut.html +8 -10
- package/docs/interfaces/TwinReference.html +4 -0
- package/docs/interfaces/TwinSpecification.html +7 -9
- package/docs/media/twin-management.md +197 -0
- package/docs/modules.html +1 -1
- package/docs/types/SinglePropertyValueType.html +1 -1
- package/docs/types/TwinBoundCapabilityName.html +8 -0
- package/docs/types/TwinCapability.html +11 -5
- package/docs/types/TwinCapabilityName.html +8 -0
- package/docs/types/TwinDiagnosticCode.html +1 -1
- package/docs/types/TwinErrorCause.html +1 -1
- package/docs/types/TwinErrorItemKind.html +1 -1
- package/docs/types/TwinPermissionsConflictCause.html +6 -0
- package/docs/types/TwinScopedCapability.html +9 -0
- package/docs/types/TwinScopedCapabilityName.html +8 -0
- package/package.json +3 -3
- package/src/TwinClient.ts +776 -15
- package/src/errors.ts +191 -4
- package/src/types/generated.ts +105 -35
- package/src/types/twin-api.ts +55 -1
- package/src/types/twin-permissions-api.ts +243 -6
package/dist/TwinClient.js
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
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, AssetAttachmentConflictError, AssetAttachmentNotFoundError, AssetBatchError, TwinFileValidationError, TwinFileConflictError, TwinFileNotFoundError, AssetCategoryValidationError, AssetCategoryConflictError, AssetCategoryNotFoundError, AssetCategoryForbiddenError, AssetTypeValidationError, AssetTypeConflictError, AssetTypeNotFoundError, AssetTypeForbiddenError, AssetCatalogTooLargeError, isAssetCatalogTooLargeErrorBody, TwinUnauthenticatedError, readErrorBody } 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, 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
|
+
}
|
|
4
40
|
export class TwinClient {
|
|
5
41
|
_httpClient;
|
|
6
42
|
_baseUrl;
|
|
@@ -21,7 +57,22 @@ export class TwinClient {
|
|
|
21
57
|
get baseUrl() {
|
|
22
58
|
return this._baseUrl;
|
|
23
59
|
}
|
|
24
|
-
async listTwins(
|
|
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
|
+
}
|
|
25
76
|
const params = new URLSearchParams();
|
|
26
77
|
if (relatedTo)
|
|
27
78
|
params.set('relatedTo', relatedTo);
|
|
@@ -29,6 +80,8 @@ export class TwinClient {
|
|
|
29
80
|
params.set('page', page);
|
|
30
81
|
if (limit !== 100)
|
|
31
82
|
params.set('limit', limit.toString());
|
|
83
|
+
if (includeDeleted)
|
|
84
|
+
params.set('includeDeleted', 'true');
|
|
32
85
|
const url = params.toString() ? `${this._baseUrl}?${params}` : this._baseUrl;
|
|
33
86
|
const response = await this._httpClient.fetch(HttpMethod.Get, url, {
|
|
34
87
|
headers: { Accept: 'application/json' },
|
|
@@ -44,13 +97,24 @@ export class TwinClient {
|
|
|
44
97
|
const errorText = await response.text();
|
|
45
98
|
throw new Error(`Failed to list twins: ${response.status} ${response.statusText} - ${errorText}`);
|
|
46
99
|
}
|
|
47
|
-
async listAllTwins(
|
|
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
|
+
}
|
|
48
112
|
const allTwins = [];
|
|
49
113
|
let page = undefined;
|
|
50
114
|
const limit = 100;
|
|
51
115
|
const maxPages = 1000;
|
|
52
116
|
for (let pageCount = 0; pageCount < maxPages; pageCount++) {
|
|
53
|
-
const response = await this.listTwins(relatedTo, page, limit, signal);
|
|
117
|
+
const response = await this.listTwins({ relatedTo, page, limit, includeDeleted }, signal);
|
|
54
118
|
allTwins.push(...response.data);
|
|
55
119
|
if (response.data.length < limit) {
|
|
56
120
|
break;
|
|
@@ -511,6 +575,13 @@ export class TwinClient {
|
|
|
511
575
|
if (response.ok) {
|
|
512
576
|
return (await response.json());
|
|
513
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
|
+
}
|
|
514
585
|
if (response.status === 400) {
|
|
515
586
|
const errorBody = await readErrorBody(response);
|
|
516
587
|
throw new AssetValidationError(`Asset batch validation failed: ${errorBody.detail}`, errorBody);
|
|
@@ -519,9 +590,12 @@ export class TwinClient {
|
|
|
519
590
|
const errorText = await response.text();
|
|
520
591
|
throw new AssetForbiddenError(`Asset batch forbidden: ${errorText}`);
|
|
521
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.
|
|
522
596
|
if (response.status === 404 || response.status === 409) {
|
|
523
|
-
const
|
|
524
|
-
throw new AssetBatchError(`Asset batch operation failed with status ${response.status}`,
|
|
597
|
+
const rolledBack = (await response.json());
|
|
598
|
+
throw new AssetBatchError(`Asset batch operation failed with status ${response.status}`, rolledBack);
|
|
525
599
|
}
|
|
526
600
|
const errorText = await response.text();
|
|
527
601
|
throw new Error(`Failed to execute asset batch: ${response.status} ${response.statusText} - ${errorText}`);
|
|
@@ -900,6 +974,247 @@ export class TwinClient {
|
|
|
900
974
|
}
|
|
901
975
|
throw new Error(`Failed to fetch permissions: ${response.status} ${response.statusText}`);
|
|
902
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
|
+
}
|
|
903
1218
|
async getAssetTypeMetaSchema(signal) {
|
|
904
1219
|
const url = `${this._assetTypeCatalogUrl}/meta-schema`;
|
|
905
1220
|
const response = await this._httpClient.fetch(HttpMethod.Get, url, {
|