@adatechnology/keycloak-admin 0.1.16 → 1.0.0-rc.1

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 ADDED
@@ -0,0 +1,63 @@
1
+ # @adatechnology/keycloak-admin
2
+
3
+ Cliente do Keycloak Admin API autenticado como **service account** (`client_credentials`).
4
+ Agnóstico de framework e de runtime: só depende de `fetch` e de `zod`.
5
+
6
+ Diferença deliberada para o `@adatechnology/nestjs-keycloak-admin`: aquele obtém token com
7
+ `grant_type=password` usando o usuário administrador do realm `master`. Este **nunca** envia senha
8
+ nem usuário — a identidade é do client confidencial, com `manage-users` do `realm-management`.
9
+
10
+ ## Uso
11
+
12
+ ```ts
13
+ import { createKeycloakAdminClient } from '@adatechnology/keycloak-admin'
14
+
15
+ const keycloak = createKeycloakAdminClient({
16
+ config: {
17
+ baseUrl: process.env.KEYCLOAK_URL,
18
+ clientId: process.env.KEYCLOAK_ADMIN_CLIENT_ID,
19
+ clientSecret: process.env.KEYCLOAK_ADMIN_CLIENT_SECRET,
20
+ realm: process.env.KEYCLOAK_REALM,
21
+ },
22
+ })
23
+
24
+ const { id } = await keycloak.createUser({
25
+ attributes: { company_id: companyId },
26
+ email: 'admin@transportadora.example',
27
+ enabled: true,
28
+ firstName: 'Ada',
29
+ lastName: 'Lovelace',
30
+ password: { temporary: false, value: chosenPassword },
31
+ username: 'admin@transportadora.example',
32
+ })
33
+ ```
34
+
35
+ Operações: `createUser`, `findUserByEmail`, `listUsers`, `updateUser`, `setEnabled`,
36
+ `updateAttributes`, `deleteUser`, `setPassword`, `setTemporaryPassword`.
37
+
38
+ `listUsers({ first, limit, search })` devolve `{ users, hasMore }`. O realm não informa total, então
39
+ a página pede um registro a mais que o limite e descarta-o: é assim que `hasMore` sai sem uma
40
+ segunda chamada.
41
+
42
+ ## Token
43
+
44
+ Obtido sob demanda, guardado em memória e renovado 30s antes de expirar
45
+ (`KEYCLOAK_ADMIN_TOKEN_RENEWAL_SKEW_MS`). Chamadas concorrentes compartilham a mesma requisição em
46
+ voo — o Keycloak recebe uma, não N.
47
+
48
+ ## Injeção
49
+
50
+ `fetch` e `now` são injetáveis, o que torna rede e relógio observáveis em teste:
51
+
52
+ ```ts
53
+ createKeycloakAdminClient({ config, fetch: stubFetch, now: clock.now })
54
+ ```
55
+
56
+ ## Erros
57
+
58
+ Toda falha vira `KeycloakAdminError` com `code` estável (`KEYCLOAK_ADMIN_ERROR_CODE`), `status` e
59
+ `context`. O contexto é montado por allowlist e passa por um redator que substitui `clientSecret`,
60
+ access token e senha por `[REDACTED]` — nem a mensagem nem o erro serializado carregam segredo.
61
+
62
+ Configuração inválida falha na construção do cliente, com o **caminho** do campo no contexto e nunca
63
+ o valor.
package/dist/index.d.ts CHANGED
@@ -1,91 +1,158 @@
1
- import { DynamicModule } from "@nestjs/common";
1
+ import { z } from 'zod';
2
2
 
3
- export interface KeycloakAdminConfig {
4
- baseUrl: string;
5
- realm: string;
6
- adminUser: string;
7
- adminPassword: string;
8
- }
9
-
10
- export interface GetAdminTokenResult {
11
- accessToken: string;
12
- expiresIn: number;
13
- tokenType: string;
14
- }
15
-
16
- export interface UpdateUserParams {
17
- userId: string;
18
- userData: Record<string, unknown>;
19
- adminToken: string;
20
- }
3
+ declare const KEYCLOAK_ADMIN_TOKEN_RENEWAL_SKEW_MS = 30000;
4
+ declare const KEYCLOAK_ADMIN_ERROR_CODE: {
5
+ readonly CONFIGURATION_INVALID: "KEYCLOAK_ADMIN_CONFIGURATION_INVALID";
6
+ readonly REQUEST_FAILED: "KEYCLOAK_ADMIN_REQUEST_FAILED";
7
+ readonly TOKEN_REQUEST_FAILED: "KEYCLOAK_ADMIN_TOKEN_REQUEST_FAILED";
8
+ readonly TOKEN_RESPONSE_INVALID: "KEYCLOAK_ADMIN_TOKEN_RESPONSE_INVALID";
9
+ readonly USER_ALREADY_EXISTS: "KEYCLOAK_ADMIN_USER_ALREADY_EXISTS";
10
+ readonly USER_ID_MISSING: "KEYCLOAK_ADMIN_USER_ID_MISSING";
11
+ readonly USER_NOT_FOUND: "KEYCLOAK_ADMIN_USER_NOT_FOUND";
12
+ };
13
+ type KeycloakAdminErrorCode = (typeof KEYCLOAK_ADMIN_ERROR_CODE)[keyof typeof KEYCLOAK_ADMIN_ERROR_CODE];
14
+ type BuildKeycloakAdminEndpointsParams = {
15
+ readonly baseUrl: string;
16
+ readonly realm: string;
17
+ };
18
+ declare function buildKeycloakAdminEndpoints({ baseUrl, realm }: BuildKeycloakAdminEndpointsParams): {
19
+ readonly token: `${string}/realms/${string}/protocol/openid-connect/token`;
20
+ readonly user: (userId: string) => string;
21
+ readonly userPassword: (userId: string) => string;
22
+ readonly users: string;
23
+ };
24
+ type KeycloakAdminEndpoints = ReturnType<typeof buildKeycloakAdminEndpoints>;
21
25
 
22
- export interface ResetPasswordParams {
23
- userId: string;
24
- password: string;
25
- temporary: boolean;
26
- adminToken: string;
26
+ type KeycloakAdminErrorContext = Readonly<Record<string, unknown>>;
27
+ type KeycloakAdminErrorParams = {
28
+ readonly code: KeycloakAdminErrorCode;
29
+ readonly context?: KeycloakAdminErrorContext;
30
+ readonly message: string;
31
+ readonly status?: number;
32
+ };
33
+ type SerializedKeycloakAdminError = {
34
+ readonly code: KeycloakAdminErrorCode;
35
+ readonly context: KeycloakAdminErrorContext;
36
+ readonly message: string;
37
+ readonly name: string;
38
+ readonly status: number | undefined;
39
+ };
40
+ /**
41
+ * Falha de qualquer operação do Admin API, com código estável para o consumidor decidir.
42
+ * O contexto é montado por allowlist e passa pelo redator — segredo, token e senha nunca entram.
43
+ */
44
+ declare class KeycloakAdminError extends Error {
45
+ readonly code: KeycloakAdminErrorCode;
46
+ readonly context: KeycloakAdminErrorContext;
47
+ readonly status: number | undefined;
48
+ constructor({ code, context, message, status }: KeycloakAdminErrorParams);
49
+ toJSON(): SerializedKeycloakAdminError;
27
50
  }
51
+ declare function isKeycloakAdminError(value: unknown): value is KeycloakAdminError;
28
52
 
29
- export interface ToggleUserEnabledParams {
30
- userId: string;
31
- enabled: boolean;
32
- adminToken: string;
33
- }
34
-
35
- export interface DeleteUserParams {
36
- userId: string;
37
- adminToken: string;
38
- }
39
-
40
- export interface UpdateUserAttributesParams {
41
- userId: string;
42
- attributes: Record<string, string | string[]>;
43
- adminToken: string;
44
- }
53
+ type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
54
+ type KeycloakAdminConfig = {
55
+ readonly baseUrl: string;
56
+ readonly clientId: string;
57
+ readonly clientSecret: string;
58
+ readonly realm: string;
59
+ };
60
+ type KeycloakUserAttributes = Readonly<Record<string, string | readonly string[]>>;
61
+ type KeycloakUser = {
62
+ readonly attributes?: KeycloakUserAttributes;
63
+ readonly email?: string;
64
+ readonly emailVerified?: boolean;
65
+ readonly enabled?: boolean;
66
+ readonly firstName?: string;
67
+ readonly id: string;
68
+ readonly lastName?: string;
69
+ readonly username?: string;
70
+ };
71
+ type KeycloakPassword = {
72
+ readonly temporary: boolean;
73
+ readonly value: string;
74
+ };
75
+ type CreateUserParams = {
76
+ readonly attributes?: KeycloakUserAttributes;
77
+ readonly email: string;
78
+ readonly emailVerified?: boolean;
79
+ readonly enabled?: boolean;
80
+ readonly firstName?: string;
81
+ readonly lastName?: string;
82
+ readonly password?: KeycloakPassword;
83
+ readonly username: string;
84
+ };
85
+ type CreateUserResult = {
86
+ readonly id: string;
87
+ };
88
+ type FindUserByEmailParams = {
89
+ readonly email: string;
90
+ };
91
+ /**
92
+ * O realm não devolve página infinita: `first`/`max` são o recorte que o Keycloak entende, e quem
93
+ * chama precisa saber se ainda há mais — daí `hasMore`, derivado de pedir um a mais que o limite.
94
+ */
95
+ type ListUsersParams = {
96
+ readonly first?: number;
97
+ readonly limit?: number;
98
+ readonly search?: string;
99
+ };
100
+ type ListUsersResult = {
101
+ readonly hasMore: boolean;
102
+ readonly users: readonly KeycloakUser[];
103
+ };
104
+ type UpdateUserParams = {
105
+ readonly user: Readonly<Partial<Pick<KeycloakUser, 'email' | 'emailVerified' | 'firstName' | 'lastName' | 'username'>>>;
106
+ readonly userId: string;
107
+ };
108
+ type SetEnabledParams = {
109
+ readonly enabled: boolean;
110
+ readonly userId: string;
111
+ };
112
+ type UpdateAttributesParams = {
113
+ readonly attributes: KeycloakUserAttributes;
114
+ readonly userId: string;
115
+ };
116
+ type DeleteUserParams = {
117
+ readonly userId: string;
118
+ };
119
+ type SetPasswordParams = {
120
+ readonly password: string;
121
+ readonly temporary: boolean;
122
+ readonly userId: string;
123
+ };
124
+ type SetTemporaryPasswordParams = {
125
+ readonly password: string;
126
+ readonly userId: string;
127
+ };
128
+ type KeycloakAdminClient = {
129
+ createUser(params: CreateUserParams): Promise<CreateUserResult>;
130
+ deleteUser(params: DeleteUserParams): Promise<void>;
131
+ findUserByEmail(params: FindUserByEmailParams): Promise<KeycloakUser | undefined>;
132
+ listUsers(params?: ListUsersParams): Promise<ListUsersResult>;
133
+ setEnabled(params: SetEnabledParams): Promise<void>;
134
+ setPassword(params: SetPasswordParams): Promise<void>;
135
+ setTemporaryPassword(params: SetTemporaryPasswordParams): Promise<void>;
136
+ updateAttributes(params: UpdateAttributesParams): Promise<void>;
137
+ updateUser(params: UpdateUserParams): Promise<void>;
138
+ };
139
+ type CreateKeycloakAdminClientParams = {
140
+ readonly config: KeycloakAdminConfig;
141
+ readonly fetch?: FetchLike;
142
+ readonly now?: () => number;
143
+ };
45
144
 
46
- export interface SendVerifyEmailParams {
47
- userId: string;
48
- adminToken: string;
49
- }
50
-
51
- export interface KeycloakAdminClientInterface {
52
- getAdminToken(): Promise<GetAdminTokenResult>;
53
- updateUser(params: UpdateUserParams): Promise<void>;
54
- resetPassword(params: ResetPasswordParams): Promise<void>;
55
- toggleUserEnabled(params: ToggleUserEnabledParams): Promise<void>;
56
- deleteUser(params: DeleteUserParams): Promise<void>;
57
- updateUserAttributes(params: UpdateUserAttributesParams): Promise<void>;
58
- sendVerifyEmail(params: SendVerifyEmailParams): Promise<void>;
59
- }
60
-
61
- export class KeycloakAdminError extends Error {
62
- readonly statusCode?: number;
63
- readonly code?: string;
64
- readonly context?: Record<string, unknown>;
65
- constructor(params: {
66
- message: string;
67
- statusCode?: number;
68
- code?: string;
69
- context?: Record<string, unknown>;
70
- });
71
- }
72
-
73
- export class KeycloakAdminClient implements KeycloakAdminClientInterface {
74
- getAdminToken(): Promise<GetAdminTokenResult>;
75
- updateUser(params: UpdateUserParams): Promise<void>;
76
- resetPassword(params: ResetPasswordParams): Promise<void>;
77
- toggleUserEnabled(params: ToggleUserEnabledParams): Promise<void>;
78
- deleteUser(params: DeleteUserParams): Promise<void>;
79
- updateUserAttributes(params: UpdateUserAttributesParams): Promise<void>;
80
- sendVerifyEmail(params: SendVerifyEmailParams): Promise<void>;
81
- }
82
-
83
- export class KeycloakAdminModule {
84
- static forRoot(config: KeycloakAdminConfig): DynamicModule;
85
- }
145
+ declare const keycloakAdminConfigSchema: z.ZodObject<{
146
+ baseUrl: z.ZodString;
147
+ clientId: z.ZodString;
148
+ clientSecret: z.ZodString;
149
+ realm: z.ZodString;
150
+ }, z.core.$strip>;
151
+ /**
152
+ * A falha carrega só o caminho do campo inválido. O valor nunca entra — seria o `clientSecret`.
153
+ */
154
+ declare function parseKeycloakAdminConfig(value: unknown): KeycloakAdminConfig;
86
155
 
87
- export function validateKeycloakAdminConfig(config: KeycloakAdminConfig): void;
156
+ declare function createKeycloakAdminClient({ config: rawConfig, fetch: injectedFetch, now, }: CreateKeycloakAdminClientParams): KeycloakAdminClient;
88
157
 
89
- export const KEYCLOAK_ADMIN_CLIENT = "KEYCLOAK_ADMIN_CLIENT";
90
- export const KEYCLOAK_ADMIN_CONFIG = "KEYCLOAK_ADMIN_CONFIG";
91
- export const KEYCLOAK_ADMIN_PROVIDER = "KEYCLOAK_ADMIN_PROVIDER";
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 };