@adatechnology/keycloak-admin 1.0.0-rc.1 → 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,9 +32,14 @@ const { id } = await keycloak.createUser({
32
32
  })
33
33
  ```
34
34
 
35
- Operações: `createUser`, `findUserByEmail`, `listUsers`, `updateUser`, `setEnabled`,
35
+ Operações de usuário: `createUser`, `findUserByEmail`, `listUsers`, `updateUser`, `setEnabled`,
36
36
  `updateAttributes`, `deleteUser`, `setPassword`, `setTemporaryPassword`.
37
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
+
38
43
  `listUsers({ first, limit, search })` devolve `{ users, hasMore }`. O realm não informa total, então
39
44
  a página pede um registro a mais que o limite e descarta-o: é assim que `hasMore` sai sem uma
40
45
  segunda chamada.
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
  };
@@ -125,7 +129,51 @@ type SetTemporaryPasswordParams = {
125
129
  readonly password: string;
126
130
  readonly userId: string;
127
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
+ };
128
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>;
129
177
  createUser(params: CreateUserParams): Promise<CreateUserResult>;
130
178
  deleteUser(params: DeleteUserParams): Promise<void>;
131
179
  findUserByEmail(params: FindUserByEmailParams): Promise<KeycloakUser | undefined>;
@@ -155,4 +203,4 @@ declare function parseKeycloakAdminConfig(value: unknown): KeycloakAdminConfig;
155
203
 
156
204
  declare function createKeycloakAdminClient({ config: rawConfig, fetch: injectedFetch, now, }: CreateKeycloakAdminClientParams): KeycloakAdminClient;
157
205
 
158
- 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 ListUsersParams, type ListUsersResult, 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) });
@@ -297,6 +302,41 @@ function createKeycloakAdminClient({
297
302
  * Pede um a mais que o limite para saber se há próxima página sem uma segunda chamada — o
298
303
  * Keycloak não devolve total, e contar o realm inteiro só para desenhar um botão é caro.
299
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
+ },
300
340
  async listUsers({ first = 0, limit = 100, search } = {}) {
301
341
  const query = new URLSearchParams({ first: String(first), max: String(limit + 1) });
302
342
  if (search !== void 0 && search !== "") query.set("search", search);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adatechnology/keycloak-admin",
3
- "version": "1.0.0-rc.1",
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",