@adatechnology/keycloak-admin 1.0.0-rc.0 → 1.0.0-rc.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.
package/README.md CHANGED
@@ -32,8 +32,17 @@ const { id } = await keycloak.createUser({
32
32
  })
33
33
  ```
34
34
 
35
- Operações: `createUser`, `findUserByEmail`, `updateUser`, `setEnabled`, `updateAttributes`,
36
- `deleteUser`, `setPassword`, `setTemporaryPassword`.
35
+ Operações de usuário: `createUser`, `findUserByEmail`, `listUsers`, `updateUser`, `setEnabled`,
36
+ `updateAttributes`, `deleteUser`, `setPassword`, `setTemporaryPassword`.
37
+
38
+ Operações de grupo: `createGroup`, `updateGroup`, `deleteGroup`, `listGroups`, `addUserToGroup`,
39
+ `removeUserFromGroup`. Só o **primeiro nível** — grupo aninhado muda o significado de "pertencer",
40
+ porque quem está no filho herda o pai, e um produto que não modela hierarquia não deve criá-la por
41
+ acidente. A filiação é endereçada pelo **usuário** (`/users/{id}/groups/{id}`), não pelo grupo.
42
+
43
+ `listUsers({ first, limit, search })` devolve `{ users, hasMore }`. O realm não informa total, então
44
+ a página pede um registro a mais que o limite e descarta-o: é assim que `hasMore` sai sem uma
45
+ segunda chamada.
37
46
 
38
47
  ## Token
39
48
 
package/dist/index.d.ts CHANGED
@@ -16,8 +16,12 @@ type BuildKeycloakAdminEndpointsParams = {
16
16
  readonly realm: string;
17
17
  };
18
18
  declare function buildKeycloakAdminEndpoints({ baseUrl, realm }: BuildKeycloakAdminEndpointsParams): {
19
+ readonly group: (groupId: string) => string;
20
+ readonly groups: string;
19
21
  readonly token: `${string}/realms/${string}/protocol/openid-connect/token`;
20
22
  readonly user: (userId: string) => string;
23
+ /** O Admin API endereça a filiação pelo usuário, não pelo grupo: é `PUT` para entrar, `DELETE` para sair. */
24
+ readonly userGroup: (userId: string, groupId: string) => string;
21
25
  readonly userPassword: (userId: string) => string;
22
26
  readonly users: string;
23
27
  };
@@ -88,6 +92,19 @@ type CreateUserResult = {
88
92
  type FindUserByEmailParams = {
89
93
  readonly email: string;
90
94
  };
95
+ /**
96
+ * O realm não devolve página infinita: `first`/`max` são o recorte que o Keycloak entende, e quem
97
+ * chama precisa saber se ainda há mais — daí `hasMore`, derivado de pedir um a mais que o limite.
98
+ */
99
+ type ListUsersParams = {
100
+ readonly first?: number;
101
+ readonly limit?: number;
102
+ readonly search?: string;
103
+ };
104
+ type ListUsersResult = {
105
+ readonly hasMore: boolean;
106
+ readonly users: readonly KeycloakUser[];
107
+ };
91
108
  type UpdateUserParams = {
92
109
  readonly user: Readonly<Partial<Pick<KeycloakUser, 'email' | 'emailVerified' | 'firstName' | 'lastName' | 'username'>>>;
93
110
  readonly userId: string;
@@ -112,10 +129,55 @@ type SetTemporaryPasswordParams = {
112
129
  readonly password: string;
113
130
  readonly userId: string;
114
131
  };
132
+ /**
133
+ * O grupo do realm. O Keycloak aceita hierarquia (`subGroups`), e este cliente trata só o primeiro
134
+ * nível de propósito: grupo aninhado muda o significado de "pertencer" — quem está no filho herda o
135
+ * pai —, e um produto que não modela hierarquia não deve criá-la por acidente.
136
+ */
137
+ type KeycloakGroup = {
138
+ readonly attributes?: KeycloakUserAttributes;
139
+ readonly id: string;
140
+ readonly name: string;
141
+ readonly path?: string;
142
+ };
143
+ type CreateGroupParams = {
144
+ readonly attributes?: KeycloakUserAttributes;
145
+ readonly name: string;
146
+ };
147
+ type CreateGroupResult = {
148
+ readonly id: string;
149
+ };
150
+ type UpdateGroupParams = {
151
+ readonly groupId: string;
152
+ readonly group: Readonly<Partial<Pick<KeycloakGroup, 'attributes' | 'name'>>>;
153
+ };
154
+ type DeleteGroupParams = {
155
+ readonly groupId: string;
156
+ };
157
+ type ListGroupsParams = {
158
+ readonly first?: number;
159
+ readonly limit?: number;
160
+ readonly search?: string;
161
+ };
162
+ type ListGroupsResult = {
163
+ readonly groups: readonly KeycloakGroup[];
164
+ readonly hasMore: boolean;
165
+ };
166
+ type GroupMembershipParams = {
167
+ readonly groupId: string;
168
+ readonly userId: string;
169
+ };
115
170
  type KeycloakAdminClient = {
171
+ addUserToGroup(params: GroupMembershipParams): Promise<void>;
172
+ createGroup(params: CreateGroupParams): Promise<CreateGroupResult>;
173
+ deleteGroup(params: DeleteGroupParams): Promise<void>;
174
+ listGroups(params?: ListGroupsParams): Promise<ListGroupsResult>;
175
+ removeUserFromGroup(params: GroupMembershipParams): Promise<void>;
176
+ updateGroup(params: UpdateGroupParams): Promise<void>;
116
177
  createUser(params: CreateUserParams): Promise<CreateUserResult>;
117
178
  deleteUser(params: DeleteUserParams): Promise<void>;
118
179
  findUserByEmail(params: FindUserByEmailParams): Promise<KeycloakUser | undefined>;
180
+ listUsers(params?: ListUsersParams): Promise<ListUsersResult>;
119
181
  setEnabled(params: SetEnabledParams): Promise<void>;
120
182
  setPassword(params: SetPasswordParams): Promise<void>;
121
183
  setTemporaryPassword(params: SetTemporaryPasswordParams): Promise<void>;
@@ -141,4 +203,4 @@ declare function parseKeycloakAdminConfig(value: unknown): KeycloakAdminConfig;
141
203
 
142
204
  declare function createKeycloakAdminClient({ config: rawConfig, fetch: injectedFetch, now, }: CreateKeycloakAdminClientParams): KeycloakAdminClient;
143
205
 
144
- export { type CreateKeycloakAdminClientParams, type CreateUserParams, type CreateUserResult, type DeleteUserParams, type FetchLike, type FindUserByEmailParams, KEYCLOAK_ADMIN_ERROR_CODE, KEYCLOAK_ADMIN_TOKEN_RENEWAL_SKEW_MS, type KeycloakAdminClient, type KeycloakAdminConfig, type KeycloakAdminEndpoints, KeycloakAdminError, type KeycloakAdminErrorCode, type KeycloakAdminErrorContext, type KeycloakAdminErrorParams, type KeycloakPassword, type KeycloakUser, type KeycloakUserAttributes, type SerializedKeycloakAdminError, type SetEnabledParams, type SetPasswordParams, type SetTemporaryPasswordParams, type UpdateAttributesParams, type UpdateUserParams, buildKeycloakAdminEndpoints, createKeycloakAdminClient, isKeycloakAdminError, keycloakAdminConfigSchema, parseKeycloakAdminConfig };
206
+ export { type CreateGroupParams, type CreateGroupResult, type CreateKeycloakAdminClientParams, type CreateUserParams, type CreateUserResult, type DeleteGroupParams, type DeleteUserParams, type FetchLike, type FindUserByEmailParams, type GroupMembershipParams, KEYCLOAK_ADMIN_ERROR_CODE, KEYCLOAK_ADMIN_TOKEN_RENEWAL_SKEW_MS, type KeycloakAdminClient, type KeycloakAdminConfig, type KeycloakAdminEndpoints, KeycloakAdminError, type KeycloakAdminErrorCode, type KeycloakAdminErrorContext, type KeycloakAdminErrorParams, type KeycloakGroup, type KeycloakPassword, type KeycloakUser, type KeycloakUserAttributes, type ListGroupsParams, type ListGroupsResult, type ListUsersParams, type ListUsersResult, type SerializedKeycloakAdminError, type SetEnabledParams, type SetPasswordParams, type SetTemporaryPasswordParams, type UpdateAttributesParams, type UpdateGroupParams, type UpdateUserParams, buildKeycloakAdminEndpoints, createKeycloakAdminClient, isKeycloakAdminError, keycloakAdminConfigSchema, parseKeycloakAdminConfig };
package/dist/index.js CHANGED
@@ -17,9 +17,14 @@ function buildKeycloakAdminEndpoints({ baseUrl, realm }) {
17
17
  const origin = baseUrl.replace(/\/+$/, "");
18
18
  const encodedRealm = encodeURIComponent(realm);
19
19
  const users = `${origin}/admin/realms/${encodedRealm}/users`;
20
+ const groups = `${origin}/admin/realms/${encodedRealm}/groups`;
20
21
  return {
22
+ group: (groupId) => `${groups}/${encodeURIComponent(groupId)}`,
23
+ groups,
21
24
  token: `${origin}/realms/${encodedRealm}/protocol/openid-connect/token`,
22
25
  user: (userId) => `${users}/${encodeURIComponent(userId)}`,
26
+ /** O Admin API endereça a filiação pelo usuário, não pelo grupo: é `PUT` para entrar, `DELETE` para sair. */
27
+ userGroup: (userId, groupId) => `${users}/${encodeURIComponent(userId)}/groups/${encodeURIComponent(groupId)}`,
23
28
  userPassword: (userId) => `${users}/${encodeURIComponent(userId)}/reset-password`,
24
29
  users
25
30
  };
@@ -195,7 +200,7 @@ function normalizeAttributes(attributes) {
195
200
  Object.entries(attributes).map(([key, value]) => [key, typeof value === "string" ? [value] : [...value]])
196
201
  );
197
202
  }
198
- function userIdFromLocation(response) {
203
+ function idFromLocation(response) {
199
204
  const location = response.headers.get("location");
200
205
  const id = location?.split("/").filter(Boolean).at(-1);
201
206
  if (id === void 0 || id === "") {
@@ -282,7 +287,7 @@ function createKeycloakAdminClient({
282
287
  secrets: [password?.value],
283
288
  url: endpoints.users
284
289
  });
285
- return { id: userIdFromLocation(response) };
290
+ return { id: idFromLocation(response) };
286
291
  },
287
292
  async deleteUser({ userId }) {
288
293
  await adminRequest({ method: "DELETE", url: endpoints.user(userId) });
@@ -293,6 +298,52 @@ function createKeycloakAdminClient({
293
298
  const found = await response.json();
294
299
  return found.at(0);
295
300
  },
301
+ /**
302
+ * Pede um a mais que o limite para saber se há próxima página sem uma segunda chamada — o
303
+ * Keycloak não devolve total, e contar o realm inteiro só para desenhar um botão é caro.
304
+ */
305
+ async addUserToGroup({ groupId, userId }) {
306
+ await adminRequest({ method: "PUT", url: endpoints.userGroup(userId, groupId) });
307
+ },
308
+ async createGroup({ attributes, name }) {
309
+ const response = await adminRequest({
310
+ body: { ...attributes === void 0 ? {} : { attributes: normalizeAttributes(attributes) }, name },
311
+ method: "POST",
312
+ url: endpoints.groups
313
+ });
314
+ return { id: idFromLocation(response) };
315
+ },
316
+ async deleteGroup({ groupId }) {
317
+ await adminRequest({ method: "DELETE", url: endpoints.group(groupId) });
318
+ },
319
+ /** Mesmo recorte de `listUsers`: pede um a mais que o limite para saber se há próxima página. */
320
+ async listGroups({ first = 0, limit = 100, search } = {}) {
321
+ const query = new URLSearchParams({ first: String(first), max: String(limit + 1) });
322
+ if (search !== void 0 && search !== "") query.set("search", search);
323
+ const response = await adminRequest({ method: "GET", url: `${endpoints.groups}?${query}` });
324
+ const found = await response.json();
325
+ return { groups: found.slice(0, limit), hasMore: found.length > limit };
326
+ },
327
+ async removeUserFromGroup({ groupId, userId }) {
328
+ await adminRequest({ method: "DELETE", url: endpoints.userGroup(userId, groupId) });
329
+ },
330
+ async updateGroup({ group, groupId }) {
331
+ await adminRequest({
332
+ body: {
333
+ ...group.attributes === void 0 ? {} : { attributes: normalizeAttributes(group.attributes) },
334
+ ...group.name === void 0 ? {} : { name: group.name }
335
+ },
336
+ method: "PUT",
337
+ url: endpoints.group(groupId)
338
+ });
339
+ },
340
+ async listUsers({ first = 0, limit = 100, search } = {}) {
341
+ const query = new URLSearchParams({ first: String(first), max: String(limit + 1) });
342
+ if (search !== void 0 && search !== "") query.set("search", search);
343
+ const response = await adminRequest({ method: "GET", url: `${endpoints.users}?${query}` });
344
+ const found = await response.json();
345
+ return { hasMore: found.length > limit, users: found.slice(0, limit) };
346
+ },
296
347
  async setEnabled({ enabled, userId }) {
297
348
  await adminRequest({ body: { enabled }, method: "PUT", url: endpoints.user(userId) });
298
349
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adatechnology/keycloak-admin",
3
- "version": "1.0.0-rc.0",
3
+ "version": "1.0.0-rc.2",
4
4
  "description": "Agnostic Keycloak Admin API client authenticated as a service account (client_credentials)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",