@twinfinity/permission 6.0.1 → 6.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,421 +1,421 @@
1
- import { TwinfinityHttpClient, HttpMethod } from '@twinfinity/authentication';
2
- import type {
3
- PaginatedResponse,
4
- User,
5
- Group,
6
- GroupMember,
7
- PutGroupRequest,
8
- DeleteGroupRequest,
9
- PermissionErrorBody
10
- } from './types';
11
- import {
12
- PermissionValidationError,
13
- PermissionError,
14
- PermissionForbiddenError,
15
- PermissionConflictError,
16
- PermissionNotFoundError
17
- } from './errors';
18
-
19
- export interface IPermissionClient {
20
- listUsers(page?: string, limit?: number, q?: string, signal?: AbortSignal): Promise<PaginatedResponse<User>>;
21
- listGroups(page?: string, limit?: number, q?: string, signal?: AbortSignal): Promise<PaginatedResponse<Group>>;
22
- putGroup(id: string, request: PutGroupRequest, signal?: AbortSignal): Promise<Group>;
23
- deleteGroup(id: string, request: DeleteGroupRequest, signal?: AbortSignal): Promise<Group>;
24
- listGroupMembers(
25
- groupId: string,
26
- page?: string,
27
- limit?: number,
28
- signal?: AbortSignal
29
- ): Promise<PaginatedResponse<User>>;
30
- addGroupMember(groupId: string, userId: string, signal?: AbortSignal): Promise<GroupMember>;
31
- removeGroupMember(groupId: string, userId: string, signal?: AbortSignal): Promise<GroupMember>;
32
- listUserGroups(
33
- userId: string,
34
- page?: string,
35
- limit?: number,
36
- signal?: AbortSignal
37
- ): Promise<PaginatedResponse<Group>>;
38
- }
39
-
40
- function isErrorBody(value: unknown): value is PermissionErrorBody {
41
- return typeof value === 'object' && value !== null && 'detail' in value && 'cause' in value;
42
- }
43
-
44
- async function readErrorBody(response: Response): Promise<PermissionErrorBody | undefined> {
45
- try {
46
- const json = await response.json();
47
- return isErrorBody(json) ? json : undefined;
48
- } catch (err) {
49
- if (err instanceof Error && err.name === 'AbortError') throw err;
50
- return undefined;
51
- }
52
- }
53
-
54
- async function readBodyText(response: Response): Promise<string> {
55
- try {
56
- return await response.text();
57
- } catch (err) {
58
- if (err instanceof Error && err.name === 'AbortError') throw err;
59
- return '';
60
- }
61
- }
62
-
63
- export class PermissionClient implements IPermissionClient {
64
- private readonly _usersUrl: string;
65
- private readonly _groupsUrl: string;
66
-
67
- constructor(
68
- baseUrl: string | URL,
69
- private readonly _httpClient: TwinfinityHttpClient
70
- ) {
71
- const base = String(baseUrl).replace(/\/+$/, '');
72
- this._usersUrl = `${base}/_ps/users`;
73
- this._groupsUrl = `${base}/_ps/groups`;
74
- }
75
-
76
- async listUsers(
77
- page: string | undefined = undefined,
78
- limit = 500,
79
- q?: string,
80
- signal?: AbortSignal
81
- ): Promise<PaginatedResponse<User>> {
82
- const params = new URLSearchParams();
83
- if (page !== undefined) params.set('page', page);
84
- params.set('limit', limit.toString());
85
- if (q) params.set('q', q);
86
-
87
- const url = `${this._usersUrl}?${params}`;
88
-
89
- const response = await this._httpClient.fetch(HttpMethod.Get, url, {
90
- signal
91
- });
92
-
93
- if (response.ok) {
94
- return (await response.json()) as PaginatedResponse<User>;
95
- }
96
-
97
- if (response.status === 400) {
98
- const body = await readErrorBody(response);
99
- throw new PermissionValidationError(body?.detail ?? 'User listing validation failed', body);
100
- }
101
-
102
- if (response.status === 403) {
103
- const body = await readErrorBody(response);
104
- throw new PermissionForbiddenError(body?.detail ?? 'Insufficient permissions to list users', body);
105
- }
106
-
107
- const errorText = await readBodyText(response);
108
- throw new PermissionError(
109
- `Failed to list users: ${response.status} ${response.statusText} - ${errorText}`,
110
- response.status,
111
- response.statusText
112
- );
113
- }
114
-
115
- async listGroups(
116
- page: string | undefined = undefined,
117
- limit = 500,
118
- q?: string,
119
- signal?: AbortSignal
120
- ): Promise<PaginatedResponse<Group>> {
121
- const params = new URLSearchParams();
122
- if (page !== undefined) params.set('page', page);
123
- params.set('limit', limit.toString());
124
- if (q) params.set('q', q);
125
-
126
- const url = `${this._groupsUrl}?${params}`;
127
-
128
- const response = await this._httpClient.fetch(HttpMethod.Get, url, {
129
- signal
130
- });
131
-
132
- if (response.ok) {
133
- return (await response.json()) as PaginatedResponse<Group>;
134
- }
135
-
136
- if (response.status === 400) {
137
- const body = await readErrorBody(response);
138
- throw new PermissionValidationError(body?.detail ?? 'Group listing validation failed', body);
139
- }
140
-
141
- if (response.status === 403) {
142
- const body = await readErrorBody(response);
143
- throw new PermissionForbiddenError(body?.detail ?? 'Insufficient permissions to list groups', body);
144
- }
145
-
146
- const errorText = await readBodyText(response);
147
- throw new PermissionError(
148
- `Failed to list groups: ${response.status} ${response.statusText} - ${errorText}`,
149
- response.status,
150
- response.statusText
151
- );
152
- }
153
-
154
- async putGroup(id: string, request: PutGroupRequest, signal?: AbortSignal): Promise<Group> {
155
- const url = `${this._groupsUrl}/${encodeURIComponent(id)}`;
156
- const response = await this._httpClient.fetch(HttpMethod.Put, url, {
157
- headers: { 'Content-Type': 'application/json' },
158
- body: JSON.stringify(request),
159
- signal
160
- });
161
-
162
- if (response.ok) {
163
- return (await response.json()) as Group;
164
- }
165
-
166
- if (response.status === 404) {
167
- const body = await readErrorBody(response);
168
- throw new PermissionNotFoundError(body?.detail ?? `Group '${id}' not found`, body);
169
- }
170
-
171
- if (response.status === 409) {
172
- let json: unknown;
173
- try {
174
- json = await response.json();
175
- } catch (err) {
176
- if (err instanceof Error && err.name === 'AbortError') throw err;
177
- throw new PermissionConflictError('Group conflict', undefined);
178
- }
179
- const body = isErrorBody(json) ? json : undefined;
180
- const data =
181
- body !== undefined && typeof json === 'object' && json !== null && 'currentState' in json
182
- ? (json as { currentState: Group }).currentState
183
- : undefined;
184
- throw new PermissionConflictError(body?.detail ?? 'Group conflict', data, body);
185
- }
186
-
187
- if (response.status === 403) {
188
- const body = await readErrorBody(response);
189
- throw new PermissionForbiddenError(
190
- body?.detail ?? 'Insufficient permissions to create or update group',
191
- body
192
- );
193
- }
194
-
195
- if (response.status === 400) {
196
- const body = await readErrorBody(response);
197
- throw new PermissionValidationError(body?.detail ?? 'Group validation failed', body);
198
- }
199
-
200
- const errorText = await readBodyText(response);
201
- throw new PermissionError(
202
- `Failed to put group: ${response.status} ${response.statusText} - ${errorText}`,
203
- response.status,
204
- response.statusText
205
- );
206
- }
207
-
208
- async listGroupMembers(
209
- groupId: string,
210
- page: string | undefined = undefined,
211
- limit = 500,
212
- signal?: AbortSignal
213
- ): Promise<PaginatedResponse<User>> {
214
- const params = new URLSearchParams();
215
- if (page !== undefined) params.set('page', page);
216
- params.set('limit', limit.toString());
217
-
218
- const url = `${this._groupsUrl}/${encodeURIComponent(groupId)}/members?${params}`;
219
-
220
- const response = await this._httpClient.fetch(HttpMethod.Get, url, { signal });
221
-
222
- if (response.ok) {
223
- return (await response.json()) as PaginatedResponse<User>;
224
- }
225
-
226
- if (response.status === 404) {
227
- const body = await readErrorBody(response);
228
- throw new PermissionNotFoundError(body?.detail ?? `Group '${groupId}' not found`, body);
229
- }
230
-
231
- if (response.status === 400) {
232
- const body = await readErrorBody(response);
233
- throw new PermissionValidationError(body?.detail ?? 'Group member listing validation failed', body);
234
- }
235
-
236
- if (response.status === 403) {
237
- const body = await readErrorBody(response);
238
- throw new PermissionForbiddenError(body?.detail ?? 'Insufficient permissions to list group members', body);
239
- }
240
-
241
- const errorText = await readBodyText(response);
242
- throw new PermissionError(
243
- `Failed to list group members: ${response.status} ${response.statusText} - ${errorText}`,
244
- response.status,
245
- response.statusText
246
- );
247
- }
248
-
249
- async addGroupMember(groupId: string, userId: string, signal?: AbortSignal): Promise<GroupMember> {
250
- const url = `${this._groupsUrl}/${encodeURIComponent(groupId)}/members/${encodeURIComponent(userId)}`;
251
-
252
- const response = await this._httpClient.fetch(HttpMethod.Put, url, { signal });
253
-
254
- if (response.ok) {
255
- return (await response.json()) as GroupMember;
256
- }
257
-
258
- if (response.status === 404) {
259
- const body = await readErrorBody(response);
260
- throw new PermissionNotFoundError(body?.detail ?? 'Group or user not found', body);
261
- }
262
-
263
- if (response.status === 400) {
264
- const body = await readErrorBody(response);
265
- throw new PermissionValidationError(body?.detail ?? 'Cannot modify this group', body);
266
- }
267
-
268
- if (response.status === 403) {
269
- const body = await readErrorBody(response);
270
- throw new PermissionForbiddenError(body?.detail ?? 'Insufficient permissions to add group member', body);
271
- }
272
-
273
- if (response.status === 409) {
274
- const body = await readErrorBody(response);
275
- throw new PermissionConflictError(
276
- body?.detail ?? 'Group membership was modified concurrently, please retry',
277
- undefined,
278
- body
279
- );
280
- }
281
-
282
- const errorText = await readBodyText(response);
283
- throw new PermissionError(
284
- `Failed to add group member: ${response.status} ${response.statusText} - ${errorText}`,
285
- response.status,
286
- response.statusText
287
- );
288
- }
289
-
290
- async removeGroupMember(groupId: string, userId: string, signal?: AbortSignal): Promise<GroupMember> {
291
- const url = `${this._groupsUrl}/${encodeURIComponent(groupId)}/members/${encodeURIComponent(userId)}`;
292
-
293
- const response = await this._httpClient.fetch(HttpMethod.Delete, url, { signal });
294
-
295
- if (response.ok) {
296
- return (await response.json()) as GroupMember;
297
- }
298
-
299
- if (response.status === 404) {
300
- const body = await readErrorBody(response);
301
- throw new PermissionNotFoundError(body?.detail ?? 'Group, user, or membership not found', body);
302
- }
303
-
304
- if (response.status === 400) {
305
- const body = await readErrorBody(response);
306
- throw new PermissionValidationError(body?.detail ?? 'Cannot modify this group', body);
307
- }
308
-
309
- if (response.status === 403) {
310
- const body = await readErrorBody(response);
311
- throw new PermissionForbiddenError(body?.detail ?? 'Insufficient permissions to remove group member', body);
312
- }
313
-
314
- if (response.status === 409) {
315
- const body = await readErrorBody(response);
316
- throw new PermissionConflictError(
317
- body?.detail ?? 'Group membership was modified concurrently, please retry',
318
- undefined,
319
- body
320
- );
321
- }
322
-
323
- const errorText = await readBodyText(response);
324
- throw new PermissionError(
325
- `Failed to remove group member: ${response.status} ${response.statusText} - ${errorText}`,
326
- response.status,
327
- response.statusText
328
- );
329
- }
330
-
331
- async listUserGroups(
332
- userId: string,
333
- page: string | undefined = undefined,
334
- limit = 500,
335
- signal?: AbortSignal
336
- ): Promise<PaginatedResponse<Group>> {
337
- const params = new URLSearchParams();
338
- if (page !== undefined) params.set('page', page);
339
- params.set('limit', limit.toString());
340
-
341
- const url = `${this._usersUrl}/${encodeURIComponent(userId)}/groups?${params}`;
342
-
343
- const response = await this._httpClient.fetch(HttpMethod.Get, url, { signal });
344
-
345
- if (response.ok) {
346
- return (await response.json()) as PaginatedResponse<Group>;
347
- }
348
-
349
- if (response.status === 404) {
350
- const body = await readErrorBody(response);
351
- throw new PermissionNotFoundError(body?.detail ?? `User '${userId}' not found`, body);
352
- }
353
-
354
- if (response.status === 400) {
355
- const body = await readErrorBody(response);
356
- throw new PermissionValidationError(body?.detail ?? 'User group listing validation failed', body);
357
- }
358
-
359
- if (response.status === 403) {
360
- const body = await readErrorBody(response);
361
- throw new PermissionForbiddenError(body?.detail ?? 'Insufficient permissions to list user groups', body);
362
- }
363
-
364
- const errorText = await readBodyText(response);
365
- throw new PermissionError(
366
- `Failed to list user groups: ${response.status} ${response.statusText} - ${errorText}`,
367
- response.status,
368
- response.statusText
369
- );
370
- }
371
-
372
- async deleteGroup(id: string, request: DeleteGroupRequest, signal?: AbortSignal): Promise<Group> {
373
- const url = `${this._groupsUrl}/${encodeURIComponent(id)}`;
374
- const response = await this._httpClient.fetch(HttpMethod.Delete, url, {
375
- headers: { 'If-Match': request.etag },
376
- signal
377
- });
378
-
379
- if (response.ok) {
380
- return (await response.json()) as Group;
381
- }
382
-
383
- if (response.status === 404) {
384
- const body = await readErrorBody(response);
385
- throw new PermissionNotFoundError(body?.detail ?? `Group '${id}' not found`, body);
386
- }
387
-
388
- if (response.status === 409) {
389
- let json: unknown;
390
- try {
391
- json = await response.json();
392
- } catch (err) {
393
- if (err instanceof Error && err.name === 'AbortError') throw err;
394
- throw new PermissionConflictError('Group was modified by another user', undefined);
395
- }
396
- const body = isErrorBody(json) ? json : undefined;
397
- const data =
398
- body !== undefined && typeof json === 'object' && json !== null && 'currentState' in json
399
- ? (json as { currentState: Group }).currentState
400
- : undefined;
401
- throw new PermissionConflictError(body?.detail ?? 'Group was modified by another user', data, body);
402
- }
403
-
404
- if (response.status === 403) {
405
- const body = await readErrorBody(response);
406
- throw new PermissionForbiddenError(body?.detail ?? 'Insufficient permissions to delete group', body);
407
- }
408
-
409
- if (response.status === 400) {
410
- const body = await readErrorBody(response);
411
- throw new PermissionValidationError(body?.detail ?? 'Group deletion validation failed', body);
412
- }
413
-
414
- const errorText = await readBodyText(response);
415
- throw new PermissionError(
416
- `Failed to delete group: ${response.status} ${response.statusText} - ${errorText}`,
417
- response.status,
418
- response.statusText
419
- );
420
- }
421
- }
1
+ import { TwinfinityHttpClient, HttpMethod } from '@twinfinity/authentication';
2
+ import type {
3
+ PaginatedResponse,
4
+ User,
5
+ Group,
6
+ GroupMember,
7
+ PutGroupRequest,
8
+ DeleteGroupRequest,
9
+ PermissionErrorBody
10
+ } from './types';
11
+ import {
12
+ PermissionValidationError,
13
+ PermissionError,
14
+ PermissionForbiddenError,
15
+ PermissionConflictError,
16
+ PermissionNotFoundError
17
+ } from './errors';
18
+
19
+ export interface IPermissionClient {
20
+ listUsers(page?: string, limit?: number, q?: string, signal?: AbortSignal): Promise<PaginatedResponse<User>>;
21
+ listGroups(page?: string, limit?: number, q?: string, signal?: AbortSignal): Promise<PaginatedResponse<Group>>;
22
+ putGroup(id: string, request: PutGroupRequest, signal?: AbortSignal): Promise<Group>;
23
+ deleteGroup(id: string, request: DeleteGroupRequest, signal?: AbortSignal): Promise<Group>;
24
+ listGroupMembers(
25
+ groupId: string,
26
+ page?: string,
27
+ limit?: number,
28
+ signal?: AbortSignal
29
+ ): Promise<PaginatedResponse<User>>;
30
+ addGroupMember(groupId: string, userId: string, signal?: AbortSignal): Promise<GroupMember>;
31
+ removeGroupMember(groupId: string, userId: string, signal?: AbortSignal): Promise<GroupMember>;
32
+ listUserGroups(
33
+ userId: string,
34
+ page?: string,
35
+ limit?: number,
36
+ signal?: AbortSignal
37
+ ): Promise<PaginatedResponse<Group>>;
38
+ }
39
+
40
+ function isErrorBody(value: unknown): value is PermissionErrorBody {
41
+ return typeof value === 'object' && value !== null && 'detail' in value && 'cause' in value;
42
+ }
43
+
44
+ async function readErrorBody(response: Response): Promise<PermissionErrorBody | undefined> {
45
+ try {
46
+ const json = await response.json();
47
+ return isErrorBody(json) ? json : undefined;
48
+ } catch (err) {
49
+ if (err instanceof Error && err.name === 'AbortError') throw err;
50
+ return undefined;
51
+ }
52
+ }
53
+
54
+ async function readBodyText(response: Response): Promise<string> {
55
+ try {
56
+ return await response.text();
57
+ } catch (err) {
58
+ if (err instanceof Error && err.name === 'AbortError') throw err;
59
+ return '';
60
+ }
61
+ }
62
+
63
+ export class PermissionClient implements IPermissionClient {
64
+ private readonly _usersUrl: string;
65
+ private readonly _groupsUrl: string;
66
+
67
+ constructor(
68
+ baseUrl: string | URL,
69
+ private readonly _httpClient: TwinfinityHttpClient
70
+ ) {
71
+ const base = String(baseUrl).replace(/\/+$/, '');
72
+ this._usersUrl = `${base}/_ps/users`;
73
+ this._groupsUrl = `${base}/_ps/groups`;
74
+ }
75
+
76
+ async listUsers(
77
+ page: string | undefined = undefined,
78
+ limit = 500,
79
+ q?: string,
80
+ signal?: AbortSignal
81
+ ): Promise<PaginatedResponse<User>> {
82
+ const params = new URLSearchParams();
83
+ if (page !== undefined) params.set('page', page);
84
+ params.set('limit', limit.toString());
85
+ if (q) params.set('q', q);
86
+
87
+ const url = `${this._usersUrl}?${params}`;
88
+
89
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
90
+ signal
91
+ });
92
+
93
+ if (response.ok) {
94
+ return (await response.json()) as PaginatedResponse<User>;
95
+ }
96
+
97
+ if (response.status === 400) {
98
+ const body = await readErrorBody(response);
99
+ throw new PermissionValidationError(body?.detail ?? 'User listing validation failed', body);
100
+ }
101
+
102
+ if (response.status === 403) {
103
+ const body = await readErrorBody(response);
104
+ throw new PermissionForbiddenError(body?.detail ?? 'Insufficient permissions to list users', body);
105
+ }
106
+
107
+ const errorText = await readBodyText(response);
108
+ throw new PermissionError(
109
+ `Failed to list users: ${response.status} ${response.statusText} - ${errorText}`,
110
+ response.status,
111
+ response.statusText
112
+ );
113
+ }
114
+
115
+ async listGroups(
116
+ page: string | undefined = undefined,
117
+ limit = 500,
118
+ q?: string,
119
+ signal?: AbortSignal
120
+ ): Promise<PaginatedResponse<Group>> {
121
+ const params = new URLSearchParams();
122
+ if (page !== undefined) params.set('page', page);
123
+ params.set('limit', limit.toString());
124
+ if (q) params.set('q', q);
125
+
126
+ const url = `${this._groupsUrl}?${params}`;
127
+
128
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, {
129
+ signal
130
+ });
131
+
132
+ if (response.ok) {
133
+ return (await response.json()) as PaginatedResponse<Group>;
134
+ }
135
+
136
+ if (response.status === 400) {
137
+ const body = await readErrorBody(response);
138
+ throw new PermissionValidationError(body?.detail ?? 'Group listing validation failed', body);
139
+ }
140
+
141
+ if (response.status === 403) {
142
+ const body = await readErrorBody(response);
143
+ throw new PermissionForbiddenError(body?.detail ?? 'Insufficient permissions to list groups', body);
144
+ }
145
+
146
+ const errorText = await readBodyText(response);
147
+ throw new PermissionError(
148
+ `Failed to list groups: ${response.status} ${response.statusText} - ${errorText}`,
149
+ response.status,
150
+ response.statusText
151
+ );
152
+ }
153
+
154
+ async putGroup(id: string, request: PutGroupRequest, signal?: AbortSignal): Promise<Group> {
155
+ const url = `${this._groupsUrl}/${encodeURIComponent(id)}`;
156
+ const response = await this._httpClient.fetch(HttpMethod.Put, url, {
157
+ headers: { 'Content-Type': 'application/json' },
158
+ body: JSON.stringify(request),
159
+ signal
160
+ });
161
+
162
+ if (response.ok) {
163
+ return (await response.json()) as Group;
164
+ }
165
+
166
+ if (response.status === 404) {
167
+ const body = await readErrorBody(response);
168
+ throw new PermissionNotFoundError(body?.detail ?? `Group '${id}' not found`, body);
169
+ }
170
+
171
+ if (response.status === 409) {
172
+ let json: unknown;
173
+ try {
174
+ json = await response.json();
175
+ } catch (err) {
176
+ if (err instanceof Error && err.name === 'AbortError') throw err;
177
+ throw new PermissionConflictError('Group conflict', undefined);
178
+ }
179
+ const body = isErrorBody(json) ? json : undefined;
180
+ const data =
181
+ body !== undefined && typeof json === 'object' && json !== null && 'currentState' in json
182
+ ? (json as { currentState: Group }).currentState
183
+ : undefined;
184
+ throw new PermissionConflictError(body?.detail ?? 'Group conflict', data, body);
185
+ }
186
+
187
+ if (response.status === 403) {
188
+ const body = await readErrorBody(response);
189
+ throw new PermissionForbiddenError(
190
+ body?.detail ?? 'Insufficient permissions to create or update group',
191
+ body
192
+ );
193
+ }
194
+
195
+ if (response.status === 400) {
196
+ const body = await readErrorBody(response);
197
+ throw new PermissionValidationError(body?.detail ?? 'Group validation failed', body);
198
+ }
199
+
200
+ const errorText = await readBodyText(response);
201
+ throw new PermissionError(
202
+ `Failed to put group: ${response.status} ${response.statusText} - ${errorText}`,
203
+ response.status,
204
+ response.statusText
205
+ );
206
+ }
207
+
208
+ async listGroupMembers(
209
+ groupId: string,
210
+ page: string | undefined = undefined,
211
+ limit = 500,
212
+ signal?: AbortSignal
213
+ ): Promise<PaginatedResponse<User>> {
214
+ const params = new URLSearchParams();
215
+ if (page !== undefined) params.set('page', page);
216
+ params.set('limit', limit.toString());
217
+
218
+ const url = `${this._groupsUrl}/${encodeURIComponent(groupId)}/members?${params}`;
219
+
220
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, { signal });
221
+
222
+ if (response.ok) {
223
+ return (await response.json()) as PaginatedResponse<User>;
224
+ }
225
+
226
+ if (response.status === 404) {
227
+ const body = await readErrorBody(response);
228
+ throw new PermissionNotFoundError(body?.detail ?? `Group '${groupId}' not found`, body);
229
+ }
230
+
231
+ if (response.status === 400) {
232
+ const body = await readErrorBody(response);
233
+ throw new PermissionValidationError(body?.detail ?? 'Group member listing validation failed', body);
234
+ }
235
+
236
+ if (response.status === 403) {
237
+ const body = await readErrorBody(response);
238
+ throw new PermissionForbiddenError(body?.detail ?? 'Insufficient permissions to list group members', body);
239
+ }
240
+
241
+ const errorText = await readBodyText(response);
242
+ throw new PermissionError(
243
+ `Failed to list group members: ${response.status} ${response.statusText} - ${errorText}`,
244
+ response.status,
245
+ response.statusText
246
+ );
247
+ }
248
+
249
+ async addGroupMember(groupId: string, userId: string, signal?: AbortSignal): Promise<GroupMember> {
250
+ const url = `${this._groupsUrl}/${encodeURIComponent(groupId)}/members/${encodeURIComponent(userId)}`;
251
+
252
+ const response = await this._httpClient.fetch(HttpMethod.Put, url, { signal });
253
+
254
+ if (response.ok) {
255
+ return (await response.json()) as GroupMember;
256
+ }
257
+
258
+ if (response.status === 404) {
259
+ const body = await readErrorBody(response);
260
+ throw new PermissionNotFoundError(body?.detail ?? 'Group or user not found', body);
261
+ }
262
+
263
+ if (response.status === 400) {
264
+ const body = await readErrorBody(response);
265
+ throw new PermissionValidationError(body?.detail ?? 'Cannot modify this group', body);
266
+ }
267
+
268
+ if (response.status === 403) {
269
+ const body = await readErrorBody(response);
270
+ throw new PermissionForbiddenError(body?.detail ?? 'Insufficient permissions to add group member', body);
271
+ }
272
+
273
+ if (response.status === 409) {
274
+ const body = await readErrorBody(response);
275
+ throw new PermissionConflictError(
276
+ body?.detail ?? 'Group membership was modified concurrently, please retry',
277
+ undefined,
278
+ body
279
+ );
280
+ }
281
+
282
+ const errorText = await readBodyText(response);
283
+ throw new PermissionError(
284
+ `Failed to add group member: ${response.status} ${response.statusText} - ${errorText}`,
285
+ response.status,
286
+ response.statusText
287
+ );
288
+ }
289
+
290
+ async removeGroupMember(groupId: string, userId: string, signal?: AbortSignal): Promise<GroupMember> {
291
+ const url = `${this._groupsUrl}/${encodeURIComponent(groupId)}/members/${encodeURIComponent(userId)}`;
292
+
293
+ const response = await this._httpClient.fetch(HttpMethod.Delete, url, { signal });
294
+
295
+ if (response.ok) {
296
+ return (await response.json()) as GroupMember;
297
+ }
298
+
299
+ if (response.status === 404) {
300
+ const body = await readErrorBody(response);
301
+ throw new PermissionNotFoundError(body?.detail ?? 'Group, user, or membership not found', body);
302
+ }
303
+
304
+ if (response.status === 400) {
305
+ const body = await readErrorBody(response);
306
+ throw new PermissionValidationError(body?.detail ?? 'Cannot modify this group', body);
307
+ }
308
+
309
+ if (response.status === 403) {
310
+ const body = await readErrorBody(response);
311
+ throw new PermissionForbiddenError(body?.detail ?? 'Insufficient permissions to remove group member', body);
312
+ }
313
+
314
+ if (response.status === 409) {
315
+ const body = await readErrorBody(response);
316
+ throw new PermissionConflictError(
317
+ body?.detail ?? 'Group membership was modified concurrently, please retry',
318
+ undefined,
319
+ body
320
+ );
321
+ }
322
+
323
+ const errorText = await readBodyText(response);
324
+ throw new PermissionError(
325
+ `Failed to remove group member: ${response.status} ${response.statusText} - ${errorText}`,
326
+ response.status,
327
+ response.statusText
328
+ );
329
+ }
330
+
331
+ async listUserGroups(
332
+ userId: string,
333
+ page: string | undefined = undefined,
334
+ limit = 500,
335
+ signal?: AbortSignal
336
+ ): Promise<PaginatedResponse<Group>> {
337
+ const params = new URLSearchParams();
338
+ if (page !== undefined) params.set('page', page);
339
+ params.set('limit', limit.toString());
340
+
341
+ const url = `${this._usersUrl}/${encodeURIComponent(userId)}/groups?${params}`;
342
+
343
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, { signal });
344
+
345
+ if (response.ok) {
346
+ return (await response.json()) as PaginatedResponse<Group>;
347
+ }
348
+
349
+ if (response.status === 404) {
350
+ const body = await readErrorBody(response);
351
+ throw new PermissionNotFoundError(body?.detail ?? `User '${userId}' not found`, body);
352
+ }
353
+
354
+ if (response.status === 400) {
355
+ const body = await readErrorBody(response);
356
+ throw new PermissionValidationError(body?.detail ?? 'User group listing validation failed', body);
357
+ }
358
+
359
+ if (response.status === 403) {
360
+ const body = await readErrorBody(response);
361
+ throw new PermissionForbiddenError(body?.detail ?? 'Insufficient permissions to list user groups', body);
362
+ }
363
+
364
+ const errorText = await readBodyText(response);
365
+ throw new PermissionError(
366
+ `Failed to list user groups: ${response.status} ${response.statusText} - ${errorText}`,
367
+ response.status,
368
+ response.statusText
369
+ );
370
+ }
371
+
372
+ async deleteGroup(id: string, request: DeleteGroupRequest, signal?: AbortSignal): Promise<Group> {
373
+ const url = `${this._groupsUrl}/${encodeURIComponent(id)}`;
374
+ const response = await this._httpClient.fetch(HttpMethod.Delete, url, {
375
+ headers: { 'If-Match': request.etag },
376
+ signal
377
+ });
378
+
379
+ if (response.ok) {
380
+ return (await response.json()) as Group;
381
+ }
382
+
383
+ if (response.status === 404) {
384
+ const body = await readErrorBody(response);
385
+ throw new PermissionNotFoundError(body?.detail ?? `Group '${id}' not found`, body);
386
+ }
387
+
388
+ if (response.status === 409) {
389
+ let json: unknown;
390
+ try {
391
+ json = await response.json();
392
+ } catch (err) {
393
+ if (err instanceof Error && err.name === 'AbortError') throw err;
394
+ throw new PermissionConflictError('Group was modified by another user', undefined);
395
+ }
396
+ const body = isErrorBody(json) ? json : undefined;
397
+ const data =
398
+ body !== undefined && typeof json === 'object' && json !== null && 'currentState' in json
399
+ ? (json as { currentState: Group }).currentState
400
+ : undefined;
401
+ throw new PermissionConflictError(body?.detail ?? 'Group was modified by another user', data, body);
402
+ }
403
+
404
+ if (response.status === 403) {
405
+ const body = await readErrorBody(response);
406
+ throw new PermissionForbiddenError(body?.detail ?? 'Insufficient permissions to delete group', body);
407
+ }
408
+
409
+ if (response.status === 400) {
410
+ const body = await readErrorBody(response);
411
+ throw new PermissionValidationError(body?.detail ?? 'Group deletion validation failed', body);
412
+ }
413
+
414
+ const errorText = await readBodyText(response);
415
+ throw new PermissionError(
416
+ `Failed to delete group: ${response.status} ${response.statusText} - ${errorText}`,
417
+ response.status,
418
+ response.statusText
419
+ );
420
+ }
421
+ }