@adatechnology/keycloak-admin 0.1.15 → 1.0.0-rc.0

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,59 @@
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`, `updateUser`, `setEnabled`, `updateAttributes`,
36
+ `deleteUser`, `setPassword`, `setTemporaryPassword`.
37
+
38
+ ## Token
39
+
40
+ Obtido sob demanda, guardado em memória e renovado 30s antes de expirar
41
+ (`KEYCLOAK_ADMIN_TOKEN_RENEWAL_SKEW_MS`). Chamadas concorrentes compartilham a mesma requisição em
42
+ voo — o Keycloak recebe uma, não N.
43
+
44
+ ## Injeção
45
+
46
+ `fetch` e `now` são injetáveis, o que torna rede e relógio observáveis em teste:
47
+
48
+ ```ts
49
+ createKeycloakAdminClient({ config, fetch: stubFetch, now: clock.now })
50
+ ```
51
+
52
+ ## Erros
53
+
54
+ Toda falha vira `KeycloakAdminError` com `code` estável (`KEYCLOAK_ADMIN_ERROR_CODE`), `status` e
55
+ `context`. O contexto é montado por allowlist e passa por um redator que substitui `clientSecret`,
56
+ access token e senha por `[REDACTED]` — nem a mensagem nem o erro serializado carregam segredo.
57
+
58
+ Configuração inválida falha na construção do cliente, com o **caminho** do campo no contexto e nunca
59
+ o valor.
package/dist/index.d.ts CHANGED
@@ -1,91 +1,144 @@
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
+ type UpdateUserParams = {
92
+ readonly user: Readonly<Partial<Pick<KeycloakUser, 'email' | 'emailVerified' | 'firstName' | 'lastName' | 'username'>>>;
93
+ readonly userId: string;
94
+ };
95
+ type SetEnabledParams = {
96
+ readonly enabled: boolean;
97
+ readonly userId: string;
98
+ };
99
+ type UpdateAttributesParams = {
100
+ readonly attributes: KeycloakUserAttributes;
101
+ readonly userId: string;
102
+ };
103
+ type DeleteUserParams = {
104
+ readonly userId: string;
105
+ };
106
+ type SetPasswordParams = {
107
+ readonly password: string;
108
+ readonly temporary: boolean;
109
+ readonly userId: string;
110
+ };
111
+ type SetTemporaryPasswordParams = {
112
+ readonly password: string;
113
+ readonly userId: string;
114
+ };
115
+ type KeycloakAdminClient = {
116
+ createUser(params: CreateUserParams): Promise<CreateUserResult>;
117
+ deleteUser(params: DeleteUserParams): Promise<void>;
118
+ findUserByEmail(params: FindUserByEmailParams): Promise<KeycloakUser | undefined>;
119
+ setEnabled(params: SetEnabledParams): Promise<void>;
120
+ setPassword(params: SetPasswordParams): Promise<void>;
121
+ setTemporaryPassword(params: SetTemporaryPasswordParams): Promise<void>;
122
+ updateAttributes(params: UpdateAttributesParams): Promise<void>;
123
+ updateUser(params: UpdateUserParams): Promise<void>;
124
+ };
125
+ type CreateKeycloakAdminClientParams = {
126
+ readonly config: KeycloakAdminConfig;
127
+ readonly fetch?: FetchLike;
128
+ readonly now?: () => number;
129
+ };
45
130
 
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
- }
131
+ declare const keycloakAdminConfigSchema: z.ZodObject<{
132
+ baseUrl: z.ZodString;
133
+ clientId: z.ZodString;
134
+ clientSecret: z.ZodString;
135
+ realm: z.ZodString;
136
+ }, z.core.$strip>;
137
+ /**
138
+ * A falha carrega só o caminho do campo inválido. O valor nunca entra — seria o `clientSecret`.
139
+ */
140
+ declare function parseKeycloakAdminConfig(value: unknown): KeycloakAdminConfig;
86
141
 
87
- export function validateKeycloakAdminConfig(config: KeycloakAdminConfig): void;
142
+ declare function createKeycloakAdminClient({ config: rawConfig, fetch: injectedFetch, now, }: CreateKeycloakAdminClientParams): KeycloakAdminClient;
88
143
 
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";
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 };
package/dist/index.js CHANGED
@@ -1,702 +1,324 @@
1
- var __defProp = Object.defineProperty;
2
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
- var __getOwnPropNames = Object.getOwnPropertyNames;
4
- var __hasOwnProp = Object.prototype.hasOwnProperty;
5
- var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
1
+ // src/keycloak-admin.constant.ts
2
+ var KEYCLOAK_ADMIN_GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials";
3
+ var KEYCLOAK_ADMIN_FORM_CONTENT_TYPE = "application/x-www-form-urlencoded";
4
+ var KEYCLOAK_ADMIN_JSON_CONTENT_TYPE = "application/json";
5
+ var KEYCLOAK_ADMIN_REDACTED = "[REDACTED]";
6
+ var KEYCLOAK_ADMIN_TOKEN_RENEWAL_SKEW_MS = 3e4;
7
+ var KEYCLOAK_ADMIN_ERROR_CODE = {
8
+ CONFIGURATION_INVALID: "KEYCLOAK_ADMIN_CONFIGURATION_INVALID",
9
+ REQUEST_FAILED: "KEYCLOAK_ADMIN_REQUEST_FAILED",
10
+ TOKEN_REQUEST_FAILED: "KEYCLOAK_ADMIN_TOKEN_REQUEST_FAILED",
11
+ TOKEN_RESPONSE_INVALID: "KEYCLOAK_ADMIN_TOKEN_RESPONSE_INVALID",
12
+ USER_ALREADY_EXISTS: "KEYCLOAK_ADMIN_USER_ALREADY_EXISTS",
13
+ USER_ID_MISSING: "KEYCLOAK_ADMIN_USER_ID_MISSING",
14
+ USER_NOT_FOUND: "KEYCLOAK_ADMIN_USER_NOT_FOUND"
9
15
  };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
-
20
- // src/index.ts
21
- var index_exports = {};
22
- __export(index_exports, {
23
- KEYCLOAK_ADMIN_CLIENT: () => KEYCLOAK_ADMIN_CLIENT,
24
- KEYCLOAK_ADMIN_CONFIG: () => KEYCLOAK_ADMIN_CONFIG,
25
- KEYCLOAK_ADMIN_PROVIDER: () => KEYCLOAK_ADMIN_PROVIDER,
26
- KeycloakAdminClient: () => KeycloakAdminClient,
27
- KeycloakAdminError: () => KeycloakAdminError,
28
- KeycloakAdminModule: () => KeycloakAdminModule,
29
- validateKeycloakAdminConfig: () => validateKeycloakAdminConfig
30
- });
31
- module.exports = __toCommonJS(index_exports);
32
-
33
- // src/keycloak-admin.module.ts
34
- var import_common2 = require("@nestjs/common");
35
- var import_http_client2 = require("@adatechnology/http-client");
36
-
37
- // src/keycloak-admin.token.ts
38
- var KEYCLOAK_ADMIN_CLIENT = "KEYCLOAK_ADMIN_CLIENT";
39
- var KEYCLOAK_ADMIN_CONFIG = "KEYCLOAK_ADMIN_CONFIG";
40
- var KEYCLOAK_ADMIN_PROVIDER = "KEYCLOAK_ADMIN_PROVIDER";
41
-
42
- // src/keycloak-admin.client.ts
43
- var import_common = require("@nestjs/common");
44
- var import_http_client = require("@adatechnology/http-client");
45
- var import_logger = require("@adatechnology/logger");
16
+ function buildKeycloakAdminEndpoints({ baseUrl, realm }) {
17
+ const origin = baseUrl.replace(/\/+$/, "");
18
+ const encodedRealm = encodeURIComponent(realm);
19
+ const users = `${origin}/admin/realms/${encodedRealm}/users`;
20
+ return {
21
+ token: `${origin}/realms/${encodedRealm}/protocol/openid-connect/token`,
22
+ user: (userId) => `${users}/${encodeURIComponent(userId)}`,
23
+ userPassword: (userId) => `${users}/${encodeURIComponent(userId)}/reset-password`,
24
+ users
25
+ };
26
+ }
46
27
 
47
- // src/errors/keycloak-admin.error.ts
48
- var _KeycloakAdminError = class _KeycloakAdminError extends Error {
49
- statusCode;
28
+ // src/keycloak-admin.error.ts
29
+ var KeycloakAdminError = class _KeycloakAdminError extends Error {
50
30
  code;
51
31
  context;
52
- constructor(params) {
53
- super(params.message);
32
+ status;
33
+ constructor({ code, context = {}, message, status }) {
34
+ super(message);
54
35
  this.name = "KeycloakAdminError";
55
- this.statusCode = params.statusCode;
56
- this.code = params.code;
57
- this.context = params.context;
36
+ this.code = code;
37
+ this.context = context;
38
+ this.status = status;
58
39
  Object.setPrototypeOf(this, _KeycloakAdminError.prototype);
59
40
  }
60
- };
61
- __name(_KeycloakAdminError, "KeycloakAdminError");
62
- var KeycloakAdminError = _KeycloakAdminError;
63
-
64
- // package.json
65
- var package_default = {
66
- name: "@adatechnology/keycloak-admin",
67
- version: "0.1.15",
68
- publishConfig: {
69
- access: "public"
70
- },
71
- main: "dist/index.js",
72
- module: "dist/index.mjs",
73
- types: "dist/index.d.ts",
74
- files: [
75
- "dist"
76
- ],
77
- scripts: {
78
- build: "rm -rf dist && tsup && cp index.d.ts dist/index.d.ts",
79
- "build:watch": "tsup --watch",
80
- check: "tsc -p tsconfig.json --noEmit"
81
- },
82
- dependencies: {
83
- "@adatechnology/http-client": "workspace:*",
84
- "@adatechnology/logger": "workspace:*"
85
- },
86
- peerDependencies: {
87
- "@nestjs/common": "^11.0.16",
88
- "@nestjs/core": "^11"
89
- },
90
- devDependencies: {
91
- "@adatechnology/shared": "workspace:*",
92
- "@esbuild-plugins/tsconfig-paths": "^0.1.2",
93
- "@swc/core": "^1.15.24",
94
- tsup: "^8.5.1",
95
- typescript: "^5.2.0"
41
+ toJSON() {
42
+ return {
43
+ code: this.code,
44
+ context: this.context,
45
+ message: this.message,
46
+ name: this.name,
47
+ status: this.status
48
+ };
96
49
  }
97
50
  };
51
+ function isKeycloakAdminError(value) {
52
+ return value instanceof KeycloakAdminError;
53
+ }
98
54
 
99
- // src/constants/keycloak-admin.constants.ts
100
- var KEYCLOAK_ADMIN_LIB_NAME = package_default.name;
101
- var KEYCLOAK_ADMIN_LIB_VERSION = package_default.version;
102
- var KEYCLOAK_ADMIN_GRANT_TYPE_PASSWORD = "password";
103
- var KEYCLOAK_ADMIN_CLIENT_ID_ADMIN_CLI = "admin-cli";
104
- var KEYCLOAK_ADMIN_CONTENT_TYPE_FORM = "application/x-www-form-urlencoded";
105
- var KEYCLOAK_ADMIN_CONTENT_TYPE_JSON = "application/json";
106
- var KEYCLOAK_ADMIN_AUTHORIZATION_HEADER = "Authorization";
107
- var KEYCLOAK_ADMIN_BEARER_PREFIX = "Bearer ";
108
- var KEYCLOAK_ADMIN_ENDPOINTS = {
109
- MASTER_TOKEN: "/realms/master/protocol/openid-connect/token",
110
- ADMIN_USERS: /* @__PURE__ */ __name((baseUrl, realm, userId) => userId ? `${baseUrl}/admin/realms/${realm}/users/${userId}` : `${baseUrl}/admin/realms/${realm}/users`, "ADMIN_USERS"),
111
- ADMIN_RESET_PASSWORD: /* @__PURE__ */ __name((baseUrl, realm, userId) => `${baseUrl}/admin/realms/${realm}/users/${userId}/reset-password`, "ADMIN_RESET_PASSWORD"),
112
- ADMIN_SEND_VERIFY_EMAIL: /* @__PURE__ */ __name((baseUrl, realm, userId) => `${baseUrl}/admin/realms/${realm}/users/${userId}/send-verify-email`, "ADMIN_SEND_VERIFY_EMAIL")
113
- };
114
- var KEYCLOAK_ADMIN_ERROR_CODES = {
115
- ADMIN_TOKEN_ERROR: "KEYCLOAK_ADMIN_TOKEN_ERROR",
116
- CREATE_USER_ERROR: "KEYCLOAK_CREATE_USER_ERROR",
117
- UPDATE_USER_ERROR: "KEYCLOAK_UPDATE_USER_ERROR",
118
- RESET_PASSWORD_ERROR: "KEYCLOAK_RESET_PASSWORD_ERROR",
119
- TOGGLE_ENABLED_ERROR: "KEYCLOAK_TOGGLE_ENABLED_ERROR",
120
- DELETE_USER_ERROR: "KEYCLOAK_DELETE_USER_ERROR",
121
- UPDATE_ATTRIBUTES_ERROR: "KEYCLOAK_UPDATE_ATTRIBUTES_ERROR",
122
- SEND_VERIFY_EMAIL_ERROR: "KEYCLOAK_SEND_VERIFY_EMAIL_ERROR"
123
- };
124
-
125
- // src/utils/extract-http-error.ts
126
- function extractHttpError(err) {
127
- const unknownErr = err;
128
- const response = unknownErr == null ? void 0 : unknownErr.response;
129
- const responseData = response == null ? void 0 : response.data;
130
- let statusCode = void 0;
131
- if (typeof (unknownErr == null ? void 0 : unknownErr.status) === "number") {
132
- statusCode = unknownErr.status;
133
- } else if (typeof (response == null ? void 0 : response.status) === "number") {
134
- statusCode = response.status;
55
+ // src/keycloak-admin.schema.ts
56
+ import { z } from "zod";
57
+ function isAbsoluteHttpUrl(value) {
58
+ try {
59
+ const url = new URL(value);
60
+ return url.protocol === "http:" || url.protocol === "https:";
61
+ } catch {
62
+ return false;
135
63
  }
136
- const details = responseData ?? (unknownErr == null ? void 0 : unknownErr.message);
137
- const rawError = (responseData == null ? void 0 : responseData.error) ?? (responseData == null ? void 0 : responseData.errorMessage);
138
- const errorCode = typeof rawError === "string" ? rawError : typeof (unknownErr == null ? void 0 : unknownErr.code) === "string" ? unknownErr.code : void 0;
139
- return {
140
- statusCode,
141
- details,
142
- errorCode
143
- };
144
64
  }
145
- __name(extractHttpError, "extractHttpError");
65
+ var keycloakAdminConfigSchema = z.object({
66
+ baseUrl: z.string().min(1).refine(isAbsoluteHttpUrl, { message: "must be an absolute http(s) URL" }),
67
+ clientId: z.string().min(1),
68
+ clientSecret: z.string().min(1),
69
+ realm: z.string().min(1)
70
+ });
71
+ function parseKeycloakAdminConfig(value) {
72
+ const parsed = keycloakAdminConfigSchema.safeParse(value);
73
+ if (parsed.success) return parsed.data;
74
+ throw new KeycloakAdminError({
75
+ code: KEYCLOAK_ADMIN_ERROR_CODE.CONFIGURATION_INVALID,
76
+ context: { fields: parsed.error.issues.map((issue) => issue.path.join(".")).sort() },
77
+ message: "Invalid Keycloak admin configuration"
78
+ });
79
+ }
146
80
 
147
- // src/keycloak-admin.client.ts
148
- function _ts_decorate(decorators, target, key, desc) {
149
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
150
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
151
- else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
152
- return c > 3 && r && Object.defineProperty(target, key, r), r;
81
+ // src/keycloak-admin.redaction.ts
82
+ function replaceAll({ secrets, text }) {
83
+ return secrets.reduce((current, secret) => current.split(secret).join(KEYCLOAK_ADMIN_REDACTED), text);
153
84
  }
154
- __name(_ts_decorate, "_ts_decorate");
155
- function _ts_metadata(k, v) {
156
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
85
+ function redactDeep({ secrets, value }) {
86
+ if (typeof value === "string") return replaceAll({ secrets, text: value });
87
+ if (Array.isArray(value)) return value.map((item) => redactDeep({ secrets, value: item }));
88
+ if (value !== null && typeof value === "object") {
89
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactDeep({ secrets, value: item })]));
90
+ }
91
+ return value;
157
92
  }
158
- __name(_ts_metadata, "_ts_metadata");
159
- function _ts_param(paramIndex, decorator) {
160
- return function(target, key) {
161
- decorator(target, key, paramIndex);
93
+ function createSecretRedactor(...secrets) {
94
+ const known = secrets.filter((secret) => typeof secret === "string" && secret !== "");
95
+ return {
96
+ text(value) {
97
+ return replaceAll({ secrets: known, text: value });
98
+ },
99
+ value(value) {
100
+ return redactDeep({ secrets: known, value });
101
+ },
102
+ with(...additional) {
103
+ return createSecretRedactor(...known, ...additional);
104
+ }
162
105
  };
163
106
  }
164
- __name(_ts_param, "_ts_param");
165
- var _KeycloakAdminClient = class _KeycloakAdminClient {
166
- config;
167
- httpProvider;
168
- logger;
169
- className = this.constructor.name;
170
- constructor(config, httpProvider, logger) {
171
- this.config = config;
172
- this.httpProvider = httpProvider;
173
- this.logger = logger;
107
+
108
+ // src/keycloak-admin.response.ts
109
+ var MAX_DETAIL_LENGTH = 200;
110
+ async function readKeycloakDetail(response) {
111
+ const text = await response.text().catch(() => "");
112
+ if (text === "") return void 0;
113
+ try {
114
+ const parsed = JSON.parse(text);
115
+ if (parsed === null || typeof parsed !== "object") return text.slice(0, MAX_DETAIL_LENGTH);
116
+ const payload = parsed;
117
+ const detail = payload.errorMessage ?? payload.error_description ?? payload.error;
118
+ return typeof detail === "string" ? detail.slice(0, MAX_DETAIL_LENGTH) : void 0;
119
+ } catch {
120
+ return text.slice(0, MAX_DETAIL_LENGTH);
174
121
  }
175
- log(level, message, libMethod, meta) {
176
- if (!this.logger) return;
177
- const payload = {
178
- message,
179
- context: this.className,
180
- lib: KEYCLOAK_ADMIN_LIB_NAME,
181
- libVersion: KEYCLOAK_ADMIN_LIB_VERSION,
182
- libMethod,
183
- meta
184
- };
185
- if (level === "info") this.logger.info(payload);
186
- else if (level === "warn") this.logger.warn(payload);
187
- else if (level === "error") this.logger.error(payload);
122
+ }
123
+
124
+ // src/keycloak-admin.token.ts
125
+ function createKeycloakTokenProvider({
126
+ config,
127
+ endpoint,
128
+ fetch: fetchImpl,
129
+ now,
130
+ redactor
131
+ }) {
132
+ let cached;
133
+ let inFlight;
134
+ function isFresh(state) {
135
+ return state.expiresAtMs - KEYCLOAK_ADMIN_TOKEN_RENEWAL_SKEW_MS > now();
188
136
  }
189
- async getAdminToken() {
190
- const method = "getAdminToken";
191
- this.log("info", `${method} - Start`, method, {
192
- realm: this.config.realm
137
+ async function requestToken() {
138
+ const requestedAtMs = now();
139
+ const response = await fetchImpl(endpoint, {
140
+ body: new URLSearchParams({
141
+ client_id: config.clientId,
142
+ client_secret: config.clientSecret,
143
+ grant_type: KEYCLOAK_ADMIN_GRANT_TYPE_CLIENT_CREDENTIALS
144
+ }),
145
+ headers: { "content-type": KEYCLOAK_ADMIN_FORM_CONTENT_TYPE },
146
+ method: "POST"
193
147
  });
194
- const url = `${this.config.baseUrl}${KEYCLOAK_ADMIN_ENDPOINTS.MASTER_TOKEN}`;
195
- const body = new URLSearchParams();
196
- body.append("grant_type", KEYCLOAK_ADMIN_GRANT_TYPE_PASSWORD);
197
- body.append("client_id", KEYCLOAK_ADMIN_CLIENT_ID_ADMIN_CLI);
198
- body.append("username", this.config.adminUser);
199
- body.append("password", this.config.adminPassword);
200
- try {
201
- const response = await this.httpProvider.post({
202
- url,
203
- data: body,
204
- config: {
205
- headers: {
206
- "Content-Type": KEYCLOAK_ADMIN_CONTENT_TYPE_FORM
207
- }
208
- }
209
- });
210
- const result = {
211
- accessToken: response.data.access_token,
212
- expiresIn: response.data.expires_in,
213
- tokenType: response.data.token_type
214
- };
215
- this.log("info", `${method} - Success`, method);
216
- return result;
217
- } catch (err) {
218
- const { statusCode, details, errorCode } = extractHttpError(err);
219
- this.log("error", `${method} - Failed`, method, {
220
- statusCode,
221
- errorCode
222
- });
148
+ if (!response.ok) {
223
149
  throw new KeycloakAdminError({
224
- message: "Keycloak admin token request failed",
225
- statusCode: statusCode ?? 502,
226
- code: KEYCLOAK_ADMIN_ERROR_CODES.ADMIN_TOKEN_ERROR,
227
- context: {
228
- url,
229
- method: "POST",
230
- details,
231
- errorCode
232
- }
150
+ code: KEYCLOAK_ADMIN_ERROR_CODE.TOKEN_REQUEST_FAILED,
151
+ context: redactor.value({ detail: await readKeycloakDetail(response), realm: config.realm }),
152
+ message: "Keycloak refused the service account token request",
153
+ status: response.status
233
154
  });
234
155
  }
235
- }
236
- async createUser(params) {
237
- var _a, _b;
238
- const method = "createUser";
239
- const { username, email, firstName, lastName, enabled, emailVerified, credentials, attributes, adminToken } = params;
240
- this.log("info", `${method} - Start`, method, {
241
- username,
242
- email
243
- });
244
- const url = KEYCLOAK_ADMIN_ENDPOINTS.ADMIN_USERS(this.config.baseUrl, this.config.realm);
245
- const userData = {
246
- username,
247
- email,
248
- firstName,
249
- lastName,
250
- enabled,
251
- emailVerified,
252
- ...credentials ? {
253
- credentials
254
- } : {},
255
- ...attributes ? {
256
- attributes
257
- } : {}
258
- };
259
- try {
260
- const response = await this.httpProvider.post({
261
- url,
262
- data: userData,
263
- config: {
264
- headers: {
265
- [KEYCLOAK_ADMIN_AUTHORIZATION_HEADER]: `${KEYCLOAK_ADMIN_BEARER_PREFIX}${adminToken}`,
266
- "Content-Type": KEYCLOAK_ADMIN_CONTENT_TYPE_JSON
267
- }
268
- }
269
- });
270
- const locationHeader = ((_a = response.headers) == null ? void 0 : _a["location"]) ?? ((_b = response.headers) == null ? void 0 : _b["Location"]);
271
- const userId = typeof locationHeader === "string" ? locationHeader.split("/").pop() ?? "" : "";
272
- this.log("info", `${method} - Success`, method, {
273
- username,
274
- email,
275
- userId
276
- });
277
- return userId;
278
- } catch (err) {
279
- const { statusCode, details, errorCode } = extractHttpError(err);
280
- this.log("error", `${method} - Failed`, method, {
281
- username,
282
- email,
283
- statusCode,
284
- errorCode
285
- });
156
+ const payload = await response.json();
157
+ if (typeof payload.access_token !== "string" || typeof payload.expires_in !== "number") {
286
158
  throw new KeycloakAdminError({
287
- message: "Keycloak create user failed",
288
- statusCode: statusCode ?? 502,
289
- code: KEYCLOAK_ADMIN_ERROR_CODES.CREATE_USER_ERROR,
290
- context: {
291
- url,
292
- method: "POST",
293
- username,
294
- email,
295
- details,
296
- errorCode
297
- }
159
+ code: KEYCLOAK_ADMIN_ERROR_CODE.TOKEN_RESPONSE_INVALID,
160
+ context: { realm: config.realm },
161
+ message: "Keycloak returned a token response without access_token or expires_in",
162
+ status: response.status
298
163
  });
299
164
  }
165
+ return {
166
+ accessToken: payload.access_token,
167
+ expiresAtMs: requestedAtMs + payload.expires_in * 1e3
168
+ };
300
169
  }
301
- async updateUser(params) {
302
- const method = "updateUser";
303
- const { userId, userData, adminToken } = params;
304
- this.log("info", `${method} - Start`, method, {
305
- userId
306
- });
307
- const url = KEYCLOAK_ADMIN_ENDPOINTS.ADMIN_USERS(this.config.baseUrl, this.config.realm, userId);
308
- try {
309
- await this.httpProvider.put({
310
- url,
311
- data: userData,
312
- config: {
313
- headers: {
314
- [KEYCLOAK_ADMIN_AUTHORIZATION_HEADER]: `${KEYCLOAK_ADMIN_BEARER_PREFIX}${adminToken}`,
315
- "Content-Type": KEYCLOAK_ADMIN_CONTENT_TYPE_JSON
316
- }
317
- }
318
- });
319
- this.log("info", `${method} - Success`, method, {
320
- userId
321
- });
322
- } catch (err) {
323
- const { statusCode, details, errorCode } = extractHttpError(err);
324
- this.log("error", `${method} - Failed`, method, {
325
- userId,
326
- statusCode,
327
- errorCode
328
- });
329
- throw new KeycloakAdminError({
330
- message: "Keycloak update user failed",
331
- statusCode: statusCode ?? 502,
332
- code: KEYCLOAK_ADMIN_ERROR_CODES.UPDATE_USER_ERROR,
333
- context: {
334
- url,
335
- method: "PUT",
336
- userId,
337
- details,
338
- errorCode
339
- }
340
- });
170
+ return {
171
+ async getAccessToken() {
172
+ if (cached && isFresh(cached)) return cached.accessToken;
173
+ inFlight ??= requestToken();
174
+ try {
175
+ const state = await inFlight;
176
+ cached = state;
177
+ return state.accessToken;
178
+ } finally {
179
+ inFlight = void 0;
180
+ }
341
181
  }
342
- }
343
- async resetPassword(params) {
344
- const method = "resetPassword";
345
- const { userId, password, temporary, adminToken } = params;
346
- this.log("info", `${method} - Start`, method, {
347
- userId,
348
- temporary
182
+ };
183
+ }
184
+
185
+ // src/keycloak-admin.client.ts
186
+ var HTTP_CONFLICT = 409;
187
+ var HTTP_NOT_FOUND = 404;
188
+ function errorCodeOf(status) {
189
+ if (status === HTTP_NOT_FOUND) return KEYCLOAK_ADMIN_ERROR_CODE.USER_NOT_FOUND;
190
+ if (status === HTTP_CONFLICT) return KEYCLOAK_ADMIN_ERROR_CODE.USER_ALREADY_EXISTS;
191
+ return KEYCLOAK_ADMIN_ERROR_CODE.REQUEST_FAILED;
192
+ }
193
+ function normalizeAttributes(attributes) {
194
+ return Object.fromEntries(
195
+ Object.entries(attributes).map(([key, value]) => [key, typeof value === "string" ? [value] : [...value]])
196
+ );
197
+ }
198
+ function userIdFromLocation(response) {
199
+ const location = response.headers.get("location");
200
+ const id = location?.split("/").filter(Boolean).at(-1);
201
+ if (id === void 0 || id === "") {
202
+ throw new KeycloakAdminError({
203
+ code: KEYCLOAK_ADMIN_ERROR_CODE.USER_ID_MISSING,
204
+ message: "Keycloak created the user without a usable Location header",
205
+ status: response.status
349
206
  });
350
- const url = KEYCLOAK_ADMIN_ENDPOINTS.ADMIN_RESET_PASSWORD(this.config.baseUrl, this.config.realm, userId);
351
- try {
352
- await this.httpProvider.put({
353
- url,
354
- data: {
355
- type: "password",
356
- value: password,
357
- temporary
358
- },
359
- config: {
360
- headers: {
361
- [KEYCLOAK_ADMIN_AUTHORIZATION_HEADER]: `${KEYCLOAK_ADMIN_BEARER_PREFIX}${adminToken}`,
362
- "Content-Type": KEYCLOAK_ADMIN_CONTENT_TYPE_JSON
363
- }
364
- }
365
- });
366
- this.log("info", `${method} - Success`, method, {
367
- userId
368
- });
369
- } catch (err) {
370
- const { statusCode, details, errorCode } = extractHttpError(err);
371
- this.log("error", `${method} - Failed`, method, {
372
- userId,
373
- statusCode,
374
- errorCode
375
- });
376
- throw new KeycloakAdminError({
377
- message: "Keycloak reset password failed",
378
- statusCode: statusCode ?? 502,
379
- code: KEYCLOAK_ADMIN_ERROR_CODES.RESET_PASSWORD_ERROR,
380
- context: {
381
- url,
382
- method: "PUT",
383
- userId,
384
- details,
385
- errorCode
386
- }
387
- });
388
- }
389
207
  }
390
- async toggleUserEnabled(params) {
391
- const method = "toggleUserEnabled";
392
- const { userId, enabled, adminToken } = params;
393
- this.log("info", `${method} - Start`, method, {
394
- userId,
395
- enabled
208
+ return id;
209
+ }
210
+ function createKeycloakAdminClient({
211
+ config: rawConfig,
212
+ fetch: injectedFetch,
213
+ now = Date.now
214
+ }) {
215
+ const config = parseKeycloakAdminConfig(rawConfig);
216
+ const endpoints = buildKeycloakAdminEndpoints(config);
217
+ const redactor = createSecretRedactor(config.clientSecret);
218
+ const fetchImpl = injectedFetch ?? ((input, init) => globalThis.fetch(input, init));
219
+ const tokenProvider = createKeycloakTokenProvider({
220
+ config,
221
+ endpoint: endpoints.token,
222
+ fetch: fetchImpl,
223
+ now,
224
+ redactor
225
+ });
226
+ async function adminRequest({ body, method, secrets = [], url }) {
227
+ const accessToken = await tokenProvider.getAccessToken();
228
+ const response = await fetchImpl(url, {
229
+ ...body === void 0 ? {} : { body: JSON.stringify(body) },
230
+ headers: {
231
+ authorization: `Bearer ${accessToken}`,
232
+ ...body === void 0 ? {} : { "content-type": KEYCLOAK_ADMIN_JSON_CONTENT_TYPE }
233
+ },
234
+ method
396
235
  });
397
- const url = KEYCLOAK_ADMIN_ENDPOINTS.ADMIN_USERS(this.config.baseUrl, this.config.realm, userId);
398
- try {
399
- await this.httpProvider.put({
400
- url,
401
- data: {
402
- enabled
403
- },
404
- config: {
405
- headers: {
406
- [KEYCLOAK_ADMIN_AUTHORIZATION_HEADER]: `${KEYCLOAK_ADMIN_BEARER_PREFIX}${adminToken}`,
407
- "Content-Type": KEYCLOAK_ADMIN_CONTENT_TYPE_JSON
408
- }
409
- }
410
- });
411
- this.log("info", `${method} - Success`, method, {
412
- userId,
413
- enabled
414
- });
415
- } catch (err) {
416
- const { statusCode, details, errorCode } = extractHttpError(err);
417
- this.log("error", `${method} - Failed`, method, {
418
- userId,
419
- enabled,
420
- statusCode,
421
- errorCode
422
- });
423
- throw new KeycloakAdminError({
424
- message: "Keycloak toggle user enabled failed",
425
- statusCode: statusCode ?? 502,
426
- code: KEYCLOAK_ADMIN_ERROR_CODES.TOGGLE_ENABLED_ERROR,
427
- context: {
428
- url,
429
- method: "PUT",
430
- userId,
431
- enabled,
432
- details,
433
- errorCode
434
- }
435
- });
436
- }
437
- }
438
- async deleteUser(params) {
439
- const method = "deleteUser";
440
- const { userId, adminToken } = params;
441
- this.log("info", `${method} - Start`, method, {
442
- userId
236
+ if (response.ok) return response;
237
+ const callRedactor = redactor.with(accessToken, ...secrets);
238
+ throw new KeycloakAdminError({
239
+ code: errorCodeOf(response.status),
240
+ context: callRedactor.value({
241
+ detail: await readKeycloakDetail(response),
242
+ method,
243
+ realm: config.realm
244
+ }),
245
+ message: callRedactor.text(`Keycloak admin request failed with status ${response.status}`),
246
+ status: response.status
443
247
  });
444
- const url = KEYCLOAK_ADMIN_ENDPOINTS.ADMIN_USERS(this.config.baseUrl, this.config.realm, userId);
445
- try {
446
- await this.httpProvider.delete({
447
- url,
448
- config: {
449
- headers: {
450
- [KEYCLOAK_ADMIN_AUTHORIZATION_HEADER]: `${KEYCLOAK_ADMIN_BEARER_PREFIX}${adminToken}`
451
- }
452
- }
453
- });
454
- this.log("info", `${method} - Success`, method, {
455
- userId
456
- });
457
- } catch (err) {
458
- const { statusCode, details, errorCode } = extractHttpError(err);
459
- this.log("error", `${method} - Failed`, method, {
460
- userId,
461
- statusCode,
462
- errorCode
463
- });
464
- throw new KeycloakAdminError({
465
- message: "Keycloak delete user failed",
466
- statusCode: statusCode ?? 502,
467
- code: KEYCLOAK_ADMIN_ERROR_CODES.DELETE_USER_ERROR,
468
- context: {
469
- url,
470
- method: "DELETE",
471
- userId,
472
- details,
473
- errorCode
474
- }
475
- });
476
- }
477
248
  }
478
- async updateUserAttributes(params) {
479
- const method = "updateUserAttributes";
480
- const { userId, attributes, adminToken } = params;
481
- this.log("info", `${method} - Start`, method, {
482
- userId,
483
- attributeKeys: Object.keys(attributes)
249
+ async function setPassword({ password, temporary, userId }) {
250
+ await adminRequest({
251
+ body: { temporary, type: "password", value: password },
252
+ method: "PUT",
253
+ secrets: [password],
254
+ url: endpoints.userPassword(userId)
484
255
  });
485
- const url = KEYCLOAK_ADMIN_ENDPOINTS.ADMIN_USERS(this.config.baseUrl, this.config.realm, userId);
486
- try {
487
- await this.httpProvider.put({
488
- url,
489
- data: {
490
- attributes
491
- },
492
- config: {
493
- headers: {
494
- [KEYCLOAK_ADMIN_AUTHORIZATION_HEADER]: `${KEYCLOAK_ADMIN_BEARER_PREFIX}${adminToken}`,
495
- "Content-Type": KEYCLOAK_ADMIN_CONTENT_TYPE_JSON
496
- }
497
- }
498
- });
499
- this.log("info", `${method} - Success`, method, {
500
- userId
501
- });
502
- } catch (err) {
503
- const { statusCode, details, errorCode } = extractHttpError(err);
504
- this.log("error", `${method} - Failed`, method, {
505
- userId,
506
- statusCode,
507
- errorCode
508
- });
509
- throw new KeycloakAdminError({
510
- message: "Keycloak update user attributes failed",
511
- statusCode: statusCode ?? 502,
512
- code: KEYCLOAK_ADMIN_ERROR_CODES.UPDATE_ATTRIBUTES_ERROR,
513
- context: {
514
- url,
515
- method: "PUT",
516
- userId,
517
- details,
518
- errorCode
519
- }
520
- });
521
- }
522
256
  }
523
- async sendVerifyEmail(params) {
524
- const method = "sendVerifyEmail";
525
- const { userId, adminToken } = params;
526
- this.log("info", `${method} - Start`, method, {
527
- userId
528
- });
529
- const url = KEYCLOAK_ADMIN_ENDPOINTS.ADMIN_SEND_VERIFY_EMAIL(this.config.baseUrl, this.config.realm, userId);
530
- try {
531
- await this.httpProvider.put({
532
- url,
533
- data: {},
534
- config: {
535
- headers: {
536
- [KEYCLOAK_ADMIN_AUTHORIZATION_HEADER]: `${KEYCLOAK_ADMIN_BEARER_PREFIX}${adminToken}`,
537
- "Content-Type": KEYCLOAK_ADMIN_CONTENT_TYPE_JSON
538
- }
539
- }
540
- });
541
- this.log("info", `${method} - Success`, method, {
542
- userId
543
- });
544
- } catch (err) {
545
- const { statusCode, details, errorCode } = extractHttpError(err);
546
- this.log("error", `${method} - Failed`, method, {
547
- userId,
548
- statusCode,
549
- errorCode
257
+ return {
258
+ async createUser({
259
+ attributes,
260
+ email,
261
+ emailVerified,
262
+ enabled,
263
+ firstName,
264
+ lastName,
265
+ password,
266
+ username
267
+ }) {
268
+ const response = await adminRequest({
269
+ body: {
270
+ ...attributes === void 0 ? {} : { attributes: normalizeAttributes(attributes) },
271
+ ...password === void 0 ? {} : {
272
+ credentials: [{ temporary: password.temporary, type: "password", value: password.value }]
273
+ },
274
+ email,
275
+ emailVerified: emailVerified ?? false,
276
+ enabled: enabled ?? true,
277
+ firstName,
278
+ lastName,
279
+ username
280
+ },
281
+ method: "POST",
282
+ secrets: [password?.value],
283
+ url: endpoints.users
550
284
  });
551
- throw new KeycloakAdminError({
552
- message: "Keycloak send verify email failed",
553
- statusCode: statusCode ?? 502,
554
- code: KEYCLOAK_ADMIN_ERROR_CODES.SEND_VERIFY_EMAIL_ERROR,
555
- context: {
556
- url,
557
- method: "PUT",
558
- userId,
559
- details,
560
- errorCode
561
- }
285
+ return { id: userIdFromLocation(response) };
286
+ },
287
+ async deleteUser({ userId }) {
288
+ await adminRequest({ method: "DELETE", url: endpoints.user(userId) });
289
+ },
290
+ async findUserByEmail({ email }) {
291
+ const query = new URLSearchParams({ email, exact: "true" });
292
+ const response = await adminRequest({ method: "GET", url: `${endpoints.users}?${query}` });
293
+ const found = await response.json();
294
+ return found.at(0);
295
+ },
296
+ async setEnabled({ enabled, userId }) {
297
+ await adminRequest({ body: { enabled }, method: "PUT", url: endpoints.user(userId) });
298
+ },
299
+ setPassword,
300
+ async setTemporaryPassword({ password, userId }) {
301
+ await setPassword({ password, temporary: true, userId });
302
+ },
303
+ async updateAttributes({ attributes, userId }) {
304
+ await adminRequest({
305
+ body: { attributes: normalizeAttributes(attributes) },
306
+ method: "PUT",
307
+ url: endpoints.user(userId)
562
308
  });
309
+ },
310
+ async updateUser({ user, userId }) {
311
+ await adminRequest({ body: user, method: "PUT", url: endpoints.user(userId) });
563
312
  }
564
- }
565
- };
566
- __name(_KeycloakAdminClient, "KeycloakAdminClient");
567
- var KeycloakAdminClient = _KeycloakAdminClient;
568
- KeycloakAdminClient = _ts_decorate([
569
- (0, import_common.Injectable)(),
570
- _ts_param(0, (0, import_common.Inject)(KEYCLOAK_ADMIN_CONFIG)),
571
- _ts_param(1, (0, import_common.Inject)(import_http_client.HTTP_PROVIDER)),
572
- _ts_param(2, (0, import_common.Optional)()),
573
- _ts_param(2, (0, import_common.Inject)(import_logger.LOGGER_PROVIDER)),
574
- _ts_metadata("design:type", Function),
575
- _ts_metadata("design:paramtypes", [
576
- typeof KeycloakAdminConfig === "undefined" ? Object : KeycloakAdminConfig,
577
- typeof HttpProviderInterface === "undefined" ? Object : HttpProviderInterface,
578
- typeof LoggerProviderInterface === "undefined" ? Object : LoggerProviderInterface
579
- ])
580
- ], KeycloakAdminClient);
581
-
582
- // src/utils/validate-config.ts
583
- function validateKeycloakAdminConfig(config) {
584
- const issues = [];
585
- if (!config) {
586
- throw new KeycloakAdminError({
587
- message: "KeycloakAdminConfig is required. Provide an object with baseUrl, realm, adminUser and adminPassword.",
588
- statusCode: 400,
589
- code: "KEYCLOAK_ADMIN_CONFIG_MISSING"
590
- });
591
- }
592
- if (typeof config.baseUrl !== "string" || config.baseUrl.trim() === "") {
593
- issues.push({
594
- field: "baseUrl",
595
- message: "baseUrl is required and must be a non-empty string (e.g. 'http://localhost:8081')"
596
- });
597
- } else if (!config.baseUrl.startsWith("http://") && !config.baseUrl.startsWith("https://")) {
598
- issues.push({
599
- field: "baseUrl",
600
- message: `baseUrl must start with 'http://' or 'https://'. Received: '${config.baseUrl}'`
601
- });
602
- }
603
- if (typeof config.realm !== "string" || config.realm.trim() === "") {
604
- issues.push({
605
- field: "realm",
606
- message: "realm is required and must be a non-empty string (e.g. 'BACKEND')"
607
- });
608
- }
609
- if (typeof config.adminUser !== "string" || config.adminUser.trim() === "") {
610
- issues.push({
611
- field: "adminUser",
612
- message: "adminUser is required and must be a non-empty string"
613
- });
614
- }
615
- if (typeof config.adminPassword !== "string" || config.adminPassword.trim() === "") {
616
- issues.push({
617
- field: "adminPassword",
618
- message: "adminPassword is required and must be a non-empty string"
619
- });
620
- }
621
- if (issues.length > 0) {
622
- const summary = issues.map((i) => ` - ${i.field}: ${i.message}`).join("\n");
623
- throw new KeycloakAdminError({
624
- message: `KeycloakAdminConfig validation failed:
625
- ${summary}`,
626
- statusCode: 400,
627
- code: "KEYCLOAK_ADMIN_CONFIG_INVALID",
628
- context: {
629
- issues
630
- }
631
- });
632
- }
633
- }
634
- __name(validateKeycloakAdminConfig, "validateKeycloakAdminConfig");
635
-
636
- // src/keycloak-admin.module.ts
637
- function _ts_decorate2(decorators, target, key, desc) {
638
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
639
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
640
- else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
641
- return c > 3 && r && Object.defineProperty(target, key, r), r;
313
+ };
642
314
  }
643
- __name(_ts_decorate2, "_ts_decorate");
644
- var _KeycloakAdminModule = class _KeycloakAdminModule {
645
- static forRoot(config) {
646
- validateKeycloakAdminConfig(config);
647
- return {
648
- module: _KeycloakAdminModule,
649
- global: true,
650
- imports: [
651
- import_http_client2.HttpModule.forRoot({
652
- baseURL: config.baseUrl,
653
- timeout: 5e3
654
- }, {
655
- logging: {
656
- enabled: true,
657
- includeBody: true,
658
- context: "KeycloakAdminHttpClient",
659
- environments: [
660
- "development",
661
- "test"
662
- ]
663
- }
664
- })
665
- ],
666
- providers: [
667
- {
668
- provide: KEYCLOAK_ADMIN_CONFIG,
669
- useValue: config
670
- },
671
- {
672
- provide: KEYCLOAK_ADMIN_CLIENT,
673
- useClass: KeycloakAdminClient
674
- },
675
- {
676
- provide: KEYCLOAK_ADMIN_PROVIDER,
677
- useExisting: KEYCLOAK_ADMIN_CLIENT
678
- }
679
- ],
680
- exports: [
681
- KEYCLOAK_ADMIN_CLIENT,
682
- KEYCLOAK_ADMIN_PROVIDER,
683
- KEYCLOAK_ADMIN_CONFIG
684
- ]
685
- };
686
- }
687
- };
688
- __name(_KeycloakAdminModule, "KeycloakAdminModule");
689
- var KeycloakAdminModule = _KeycloakAdminModule;
690
- KeycloakAdminModule = _ts_decorate2([
691
- (0, import_common2.Module)({})
692
- ], KeycloakAdminModule);
693
- // Annotate the CommonJS export names for ESM import in node:
694
- 0 && (module.exports = {
695
- KEYCLOAK_ADMIN_CLIENT,
696
- KEYCLOAK_ADMIN_CONFIG,
697
- KEYCLOAK_ADMIN_PROVIDER,
698
- KeycloakAdminClient,
315
+ export {
316
+ KEYCLOAK_ADMIN_ERROR_CODE,
317
+ KEYCLOAK_ADMIN_TOKEN_RENEWAL_SKEW_MS,
699
318
  KeycloakAdminError,
700
- KeycloakAdminModule,
701
- validateKeycloakAdminConfig
702
- });
319
+ buildKeycloakAdminEndpoints,
320
+ createKeycloakAdminClient,
321
+ isKeycloakAdminError,
322
+ keycloakAdminConfigSchema,
323
+ parseKeycloakAdminConfig
324
+ };
package/package.json CHANGED
@@ -1,33 +1,42 @@
1
1
  {
2
2
  "name": "@adatechnology/keycloak-admin",
3
- "version": "0.1.15",
4
- "publishConfig": {
5
- "access": "public"
6
- },
3
+ "version": "1.0.0-rc.0",
4
+ "description": "Agnostic Keycloak Admin API client authenticated as a service account (client_credentials)",
5
+ "type": "module",
7
6
  "main": "dist/index.js",
8
- "module": "dist/index.mjs",
9
7
  "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
10
14
  "files": [
11
15
  "dist"
12
16
  ],
13
- "dependencies": {
14
- "@adatechnology/http-client": "0.0.23",
15
- "@adatechnology/logger": "0.0.19"
16
- },
17
+ "keywords": [
18
+ "keycloak",
19
+ "admin",
20
+ "client-credentials",
21
+ "service-account",
22
+ "identity"
23
+ ],
17
24
  "peerDependencies": {
18
- "@nestjs/common": "^11.0.16",
19
- "@nestjs/core": "^11"
25
+ "zod": "^3.24.1 || ^4.0.0"
20
26
  },
21
27
  "devDependencies": {
22
- "@esbuild-plugins/tsconfig-paths": "^0.1.2",
23
- "@swc/core": "^1.15.24",
24
- "tsup": "^8.5.1",
25
- "typescript": "^5.2.0",
26
- "@adatechnology/shared": "0.0.2"
28
+ "@types/bun": "latest",
29
+ "tsup": "^8.0.0",
30
+ "typescript": "^5.0.0",
31
+ "zod": "^4.4.3"
32
+ },
33
+ "license": "MIT",
34
+ "publishConfig": {
35
+ "access": "public"
27
36
  },
28
37
  "scripts": {
29
- "build": "rm -rf dist && tsup && cp index.d.ts dist/index.d.ts",
30
- "build:watch": "tsup --watch",
31
- "check": "tsc -p tsconfig.json --noEmit"
38
+ "build": "tsup",
39
+ "check": "tsc --noEmit",
40
+ "test": "bun test"
32
41
  }
33
42
  }