@twinfinity/permission 6.1.0 → 6.1.2-ci.30738

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.
@@ -6,6 +6,8 @@ import type {
6
6
  GroupMember,
7
7
  PutGroupRequest,
8
8
  DeleteGroupRequest,
9
+ ListUsersOptions,
10
+ GetUserOptions,
9
11
  PermissionErrorBody
10
12
  } from './types';
11
13
  import {
@@ -17,7 +19,24 @@ import {
17
19
  } from './errors';
18
20
 
19
21
  export interface IPermissionClient {
22
+ /**
23
+ * Lists users with keyset pagination and optional filters. `q` is a case-insensitive fuzzy
24
+ * search across external username, email, first and last name; `externalUsername` and
25
+ * `email` are case-insensitive exact matches. All filters are AND-combined.
26
+ *
27
+ * @throws {PermissionValidationError} On invalid pagination token, out-of-range limit, or too-long filter (400).
28
+ * @throws {PermissionForbiddenError} On insufficient permissions, including `includeDeleted` without global `Edit` (403).
29
+ */
30
+ listUsers(options?: ListUsersOptions, signal?: AbortSignal): Promise<PaginatedResponse<User>>;
20
31
  listUsers(page?: string, limit?: number, q?: string, signal?: AbortSignal): Promise<PaginatedResponse<User>>;
32
+ /**
33
+ * Gets a single user by their unique ID. Soft-deleted users are reported as not found
34
+ * unless `includeDeleted` is set.
35
+ *
36
+ * @throws {PermissionNotFoundError} When no such user exists, or the user is soft-deleted and `includeDeleted` is false (404).
37
+ * @throws {PermissionForbiddenError} On insufficient permissions, including `includeDeleted` without global `Edit` (403).
38
+ */
39
+ getUser(userId: string, options?: GetUserOptions, signal?: AbortSignal): Promise<User>;
21
40
  listGroups(page?: string, limit?: number, q?: string, signal?: AbortSignal): Promise<PaginatedResponse<Group>>;
22
41
  putGroup(id: string, request: PutGroupRequest, signal?: AbortSignal): Promise<Group>;
23
42
  deleteGroup(id: string, request: DeleteGroupRequest, signal?: AbortSignal): Promise<Group>;
@@ -73,21 +92,40 @@ export class PermissionClient implements IPermissionClient {
73
92
  this._groupsUrl = `${base}/_ps/groups`;
74
93
  }
75
94
 
95
+ listUsers(options?: ListUsersOptions, signal?: AbortSignal): Promise<PaginatedResponse<User>>;
96
+ listUsers(page?: string, limit?: number, q?: string, signal?: AbortSignal): Promise<PaginatedResponse<User>>;
76
97
  async listUsers(
77
- page: string | undefined = undefined,
78
- limit = 500,
98
+ pageOrOptions?: string | ListUsersOptions,
99
+ limitOrSignal?: number | AbortSignal,
79
100
  q?: string,
80
101
  signal?: AbortSignal
81
102
  ): Promise<PaginatedResponse<User>> {
103
+ let options: ListUsersOptions;
104
+ let abortSignal: AbortSignal | undefined;
105
+ const secondArgIsSignal = limitOrSignal !== undefined && typeof limitOrSignal !== 'number';
106
+ if (typeof pageOrOptions === 'object') {
107
+ options = pageOrOptions;
108
+ abortSignal = secondArgIsSignal ? limitOrSignal : undefined;
109
+ } else if (secondArgIsSignal) {
110
+ options = { page: pageOrOptions };
111
+ abortSignal = limitOrSignal;
112
+ } else {
113
+ options = { page: pageOrOptions, limit: limitOrSignal, q };
114
+ abortSignal = signal;
115
+ }
116
+
82
117
  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);
118
+ if (options.page !== undefined) params.set('page', options.page);
119
+ params.set('limit', (options.limit ?? 500).toString());
120
+ if (options.q) params.set('q', options.q);
121
+ if (options.externalUsername) params.set('externalUsername', options.externalUsername);
122
+ if (options.email) params.set('email', options.email);
123
+ if (options.includeDeleted) params.set('includeDeleted', 'true');
86
124
 
87
125
  const url = `${this._usersUrl}?${params}`;
88
126
 
89
127
  const response = await this._httpClient.fetch(HttpMethod.Get, url, {
90
- signal
128
+ signal: abortSignal
91
129
  });
92
130
 
93
131
  if (response.ok) {
@@ -112,6 +150,37 @@ export class PermissionClient implements IPermissionClient {
112
150
  );
113
151
  }
114
152
 
153
+ async getUser(userId: string, options?: GetUserOptions, signal?: AbortSignal): Promise<User> {
154
+ const params = new URLSearchParams();
155
+ if (options?.includeDeleted) params.set('includeDeleted', 'true');
156
+ const query = params.toString();
157
+
158
+ const url = `${this._usersUrl}/${encodeURIComponent(userId)}${query ? `?${query}` : ''}`;
159
+
160
+ const response = await this._httpClient.fetch(HttpMethod.Get, url, { signal });
161
+
162
+ if (response.ok) {
163
+ return (await response.json()) as User;
164
+ }
165
+
166
+ if (response.status === 404) {
167
+ const body = await readErrorBody(response);
168
+ throw new PermissionNotFoundError(body?.detail ?? `User '${userId}' not found`, body);
169
+ }
170
+
171
+ if (response.status === 403) {
172
+ const body = await readErrorBody(response);
173
+ throw new PermissionForbiddenError(body?.detail ?? 'Insufficient permissions to get user', body);
174
+ }
175
+
176
+ const errorText = await readBodyText(response);
177
+ throw new PermissionError(
178
+ `Failed to get user: ${response.status} ${response.statusText} - ${errorText}`,
179
+ response.status,
180
+ response.statusText
181
+ );
182
+ }
183
+
115
184
  async listGroups(
116
185
  page: string | undefined = undefined,
117
186
  limit = 500,
package/src/types.ts CHANGED
@@ -20,9 +20,63 @@ export interface PaginatedResponse<T> {
20
20
  export interface User {
21
21
  id: string;
22
22
  username: string;
23
+ /**
24
+ * The human-readable username from the user's source pool (e.g. PS UserName). Omitted when
25
+ * not available — the backend drops null fields from responses. Unlike `username` (an opaque
26
+ * per-pool key), this is the name users recognize; it is not unique.
27
+ */
28
+ externalUsername?: string;
23
29
  email: string;
24
- firstName: string | null;
25
- lastName: string | null;
30
+ firstName?: string;
31
+ lastName?: string;
32
+ /**
33
+ * `true` when the user is soft-deleted; omitted otherwise. Only ever present on responses
34
+ * requested with `includeDeleted: true`, since deleted users are hidden by default.
35
+ */
36
+ deleted?: boolean;
37
+ }
38
+
39
+ /**
40
+ * Filters for {@link IPermissionClient.listUsers | listUsers}. All filters are AND-combined.
41
+ */
42
+ export interface ListUsersOptions {
43
+ /** Pagination token from a previous response's `pagination.nextPage`. */
44
+ page?: string;
45
+ /** Maximum number of items to return (max 1000). Defaults to 500. */
46
+ limit?: number;
47
+ /**
48
+ * Case-insensitive free-text search matched against external username, email, first name,
49
+ * and last name (never the opaque `username`). Empty strings are treated as "no filter".
50
+ */
51
+ q?: string;
52
+ /**
53
+ * Case-insensitive exact match on external username. Unlike the fuzzy `q` search this never
54
+ * matches substrings. May yield 0..n users — the field is not unique. Empty strings are
55
+ * treated as "no filter".
56
+ */
57
+ externalUsername?: string;
58
+ /**
59
+ * Case-insensitive exact match on email. Unlike the fuzzy `q` search this never matches
60
+ * substrings. May yield 0..n users — the field is not unique. Empty strings are treated
61
+ * as "no filter".
62
+ */
63
+ email?: string;
64
+ /**
65
+ * When true, includes soft-deleted users. Requires `Edit` on the global
66
+ * principal-management scope; the request fails with 403 otherwise.
67
+ */
68
+ includeDeleted?: boolean;
69
+ }
70
+
71
+ /**
72
+ * Options for {@link IPermissionClient.getUser | getUser}.
73
+ */
74
+ export interface GetUserOptions {
75
+ /**
76
+ * When true, allows returning a soft-deleted user. Requires `Edit` on the global
77
+ * principal-management scope; the request fails with 403 otherwise.
78
+ */
79
+ includeDeleted?: boolean;
26
80
  }
27
81
 
28
82
  /**
@@ -37,7 +91,8 @@ export interface GroupMember extends User {
37
91
  export enum PrincipalType {
38
92
  Unknown = 'Unknown',
39
93
  DomainGroup = 'DomainGroup',
40
- Group = 'Group'
94
+ Group = 'Group',
95
+ User = 'User'
41
96
  }
42
97
 
43
98
  /**