@happyvertical/auth 0.74.8

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.
Files changed (45) hide show
  1. package/AGENT.md +33 -0
  2. package/LICENSE +7 -0
  3. package/README.md +73 -0
  4. package/dist/chunks/cognito-dmypylFX.js +128 -0
  5. package/dist/chunks/cognito-dmypylFX.js.map +1 -0
  6. package/dist/chunks/decode_jwt-D2OK1b8a.js +1395 -0
  7. package/dist/chunks/decode_jwt-D2OK1b8a.js.map +1 -0
  8. package/dist/chunks/github-NSZp5tVm.js +413 -0
  9. package/dist/chunks/github-NSZp5tVm.js.map +1 -0
  10. package/dist/chunks/google-HXk2ctYR.js +483 -0
  11. package/dist/chunks/google-HXk2ctYR.js.map +1 -0
  12. package/dist/chunks/index-BpsMhFXS.js +151 -0
  13. package/dist/chunks/index-BpsMhFXS.js.map +1 -0
  14. package/dist/chunks/kanidm-hkw-YPVF.js +747 -0
  15. package/dist/chunks/kanidm-hkw-YPVF.js.map +1 -0
  16. package/dist/chunks/keycloak-t6JEUeOz.js +871 -0
  17. package/dist/chunks/keycloak-t6JEUeOz.js.map +1 -0
  18. package/dist/cli/claude-context.d.ts +3 -0
  19. package/dist/cli/claude-context.d.ts.map +1 -0
  20. package/dist/cli/claude-context.js +21 -0
  21. package/dist/cli/claude-context.js.map +1 -0
  22. package/dist/index.d.ts +65 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +499 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/shared/errors.d.ts +227 -0
  27. package/dist/shared/errors.d.ts.map +1 -0
  28. package/dist/shared/factory.d.ts +85 -0
  29. package/dist/shared/factory.d.ts.map +1 -0
  30. package/dist/shared/providers/cognito.d.ts +38 -0
  31. package/dist/shared/providers/cognito.d.ts.map +1 -0
  32. package/dist/shared/providers/github.d.ts +65 -0
  33. package/dist/shared/providers/github.d.ts.map +1 -0
  34. package/dist/shared/providers/google.d.ts +58 -0
  35. package/dist/shared/providers/google.d.ts.map +1 -0
  36. package/dist/shared/providers/kanidm.d.ts +78 -0
  37. package/dist/shared/providers/kanidm.d.ts.map +1 -0
  38. package/dist/shared/providers/keycloak.d.ts +67 -0
  39. package/dist/shared/providers/keycloak.d.ts.map +1 -0
  40. package/dist/shared/providers/nostr/index.d.ts +47 -0
  41. package/dist/shared/providers/nostr/index.d.ts.map +1 -0
  42. package/dist/shared/types.d.ts +812 -0
  43. package/dist/shared/types.d.ts.map +1 -0
  44. package/metadata.json +32 -0
  45. package/package.json +60 -0
@@ -0,0 +1,871 @@
1
+ import { ConfigurationError, NetworkError, InvalidCredentialsError, AccessDeniedError, InvalidGrantError, InvalidClientError, UserNotFoundError, ProviderError, UserAlreadyExistsError, MfaRequiredError, InvalidNonceError, TokenExpiredError, InvalidTokenError, NotImplementedError } from "../index.js";
2
+ import { c as createRemoteJWKSet, d as decodeJwt, j as jwtVerify, J as JWTExpired, a as JWTClaimValidationFailed, b as JWSSignatureVerificationFailed } from "./decode_jwt-D2OK1b8a.js";
3
+ function generateRandomString(length = 32) {
4
+ const array = new Uint8Array(length);
5
+ crypto.getRandomValues(array);
6
+ return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join(
7
+ ""
8
+ );
9
+ }
10
+ async function generatePKCE() {
11
+ const verifier = generateRandomString(32);
12
+ const encoder = new TextEncoder();
13
+ const data = encoder.encode(verifier);
14
+ const hash = await crypto.subtle.digest("SHA-256", data);
15
+ const challenge = btoa(String.fromCharCode(...new Uint8Array(hash))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
16
+ return { verifier, challenge };
17
+ }
18
+ class KeycloakProvider {
19
+ options;
20
+ discoveryDocument = null;
21
+ jwks = null;
22
+ constructor(options) {
23
+ if (!options.serverUrl) {
24
+ throw new ConfigurationError("serverUrl is required", "keycloak");
25
+ }
26
+ if (!options.realm) {
27
+ throw new ConfigurationError("realm is required", "keycloak");
28
+ }
29
+ if (!options.clientId) {
30
+ throw new ConfigurationError("clientId is required", "keycloak");
31
+ }
32
+ this.options = {
33
+ usePKCE: true,
34
+ verifySsl: true,
35
+ scopes: ["openid", "profile", "email"],
36
+ timeout: 3e4,
37
+ maxRetries: 3,
38
+ ...options
39
+ };
40
+ }
41
+ // ---------------------------------------------------------------------------
42
+ // INTERNAL HELPERS
43
+ // ---------------------------------------------------------------------------
44
+ /**
45
+ * Get the base URL for the realm.
46
+ */
47
+ getRealmUrl() {
48
+ return `${this.options.serverUrl}/realms/${this.options.realm}`;
49
+ }
50
+ /**
51
+ * Get the admin API base URL.
52
+ */
53
+ getAdminUrl() {
54
+ return `${this.options.serverUrl}/admin/realms/${this.options.realm}`;
55
+ }
56
+ /**
57
+ * Make an HTTP request with error handling.
58
+ */
59
+ async request(url, options = {}, adminToken) {
60
+ const headers = {
61
+ "Content-Type": "application/json",
62
+ ...this.options.headers,
63
+ ...options.headers
64
+ };
65
+ if (adminToken) {
66
+ headers["Authorization"] = `Bearer ${adminToken}`;
67
+ }
68
+ try {
69
+ const response = await fetch(url, {
70
+ ...options,
71
+ headers,
72
+ signal: AbortSignal.timeout(this.options.timeout || 3e4)
73
+ });
74
+ if (!response.ok) {
75
+ const errorBody = await response.text().catch(() => "");
76
+ let errorData = {};
77
+ try {
78
+ errorData = JSON.parse(errorBody);
79
+ } catch {
80
+ }
81
+ this.handleHttpError(response.status, errorData, errorBody);
82
+ }
83
+ const text = await response.text();
84
+ if (!text) return {};
85
+ return JSON.parse(text);
86
+ } catch (error) {
87
+ if (error instanceof Error && error.name === "TimeoutError") {
88
+ throw new NetworkError("Request timed out", "keycloak", error);
89
+ }
90
+ if (error instanceof InvalidCredentialsError || error instanceof AccessDeniedError || error instanceof InvalidGrantError || error instanceof InvalidClientError || error instanceof UserNotFoundError || error instanceof ProviderError) {
91
+ throw error;
92
+ }
93
+ throw new NetworkError(
94
+ `Network error: ${error instanceof Error ? error.message : "Unknown error"}`,
95
+ "keycloak",
96
+ error instanceof Error ? error : void 0
97
+ );
98
+ }
99
+ }
100
+ /**
101
+ * Handle HTTP error responses.
102
+ */
103
+ handleHttpError(status, data, rawBody) {
104
+ const error = data.error;
105
+ const errorDescription = data.errorMessage || data.error_description || rawBody;
106
+ switch (status) {
107
+ case 400:
108
+ if (error === "invalid_grant") {
109
+ throw new InvalidGrantError(errorDescription, "keycloak");
110
+ }
111
+ if (error === "invalid_client") {
112
+ throw new InvalidClientError("keycloak");
113
+ }
114
+ throw new ProviderError(`Bad request: ${errorDescription}`, "keycloak");
115
+ case 401:
116
+ throw new InvalidCredentialsError("keycloak");
117
+ case 403:
118
+ throw new AccessDeniedError(errorDescription, "keycloak");
119
+ case 404:
120
+ throw new UserNotFoundError(void 0, "keycloak");
121
+ case 409:
122
+ throw new UserAlreadyExistsError(void 0, "keycloak");
123
+ default:
124
+ throw new ProviderError(
125
+ `Keycloak error (${status}): ${errorDescription}`,
126
+ "keycloak"
127
+ );
128
+ }
129
+ }
130
+ /**
131
+ * Fetch and cache the OIDC discovery document.
132
+ */
133
+ async fetchDiscoveryDocument() {
134
+ if (this.discoveryDocument) {
135
+ return this.discoveryDocument;
136
+ }
137
+ const url = `${this.getRealmUrl()}/.well-known/openid-configuration`;
138
+ this.discoveryDocument = await this.request(url);
139
+ return this.discoveryDocument;
140
+ }
141
+ /**
142
+ * Get JWKS for token validation.
143
+ */
144
+ async getJWKS() {
145
+ if (this.jwks) {
146
+ return this.jwks;
147
+ }
148
+ const discovery = await this.fetchDiscoveryDocument();
149
+ this.jwks = createRemoteJWKSet(new URL(discovery.jwks_uri));
150
+ return this.jwks;
151
+ }
152
+ // ---------------------------------------------------------------------------
153
+ // AUTHENTICATION FLOWS
154
+ // ---------------------------------------------------------------------------
155
+ async getAuthorizationUrl(options) {
156
+ const discovery = await this.fetchDiscoveryDocument();
157
+ const state = options?.state || generateRandomString();
158
+ const nonce = options?.nonce || generateRandomString();
159
+ const scopes = options?.scopes || this.options.scopes || ["openid", "profile", "email"];
160
+ const redirectUri = options?.redirectUri || this.options.redirectUri;
161
+ if (!redirectUri) {
162
+ throw new ConfigurationError("redirectUri is required", "keycloak");
163
+ }
164
+ const params = new URLSearchParams({
165
+ client_id: this.options.clientId,
166
+ redirect_uri: redirectUri,
167
+ response_type: "code",
168
+ scope: scopes.join(" "),
169
+ state,
170
+ nonce
171
+ });
172
+ if (options?.prompt) {
173
+ params.set("prompt", options.prompt);
174
+ }
175
+ if (options?.loginHint) {
176
+ params.set("login_hint", options.loginHint);
177
+ }
178
+ if (options?.extraParams) {
179
+ for (const [key, value] of Object.entries(options.extraParams)) {
180
+ params.set(key, value);
181
+ }
182
+ }
183
+ let codeVerifier;
184
+ if (this.options.usePKCE) {
185
+ const pkce = await generatePKCE();
186
+ codeVerifier = pkce.verifier;
187
+ params.set("code_challenge", pkce.challenge);
188
+ params.set("code_challenge_method", "S256");
189
+ }
190
+ const url = `${discovery.authorization_endpoint}?${params.toString()}`;
191
+ return {
192
+ url,
193
+ state,
194
+ nonce,
195
+ codeVerifier
196
+ };
197
+ }
198
+ async exchangeCode(params) {
199
+ const discovery = await this.fetchDiscoveryDocument();
200
+ const redirectUri = params.redirectUri || this.options.redirectUri;
201
+ if (!redirectUri) {
202
+ throw new ConfigurationError("redirectUri is required", "keycloak");
203
+ }
204
+ const body = new URLSearchParams({
205
+ grant_type: "authorization_code",
206
+ client_id: this.options.clientId,
207
+ code: params.code,
208
+ redirect_uri: redirectUri
209
+ });
210
+ if (this.options.clientSecret) {
211
+ body.set("client_secret", this.options.clientSecret);
212
+ }
213
+ if (params.codeVerifier) {
214
+ body.set("code_verifier", params.codeVerifier);
215
+ }
216
+ const response = await this.request(discovery.token_endpoint, {
217
+ method: "POST",
218
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
219
+ body: body.toString()
220
+ });
221
+ let userId = "";
222
+ if (response.id_token) {
223
+ const payload = decodeJwt(response.id_token);
224
+ userId = payload.sub || "";
225
+ }
226
+ return {
227
+ accessToken: response.access_token,
228
+ tokenType: response.token_type || "Bearer",
229
+ expiresIn: response.expires_in,
230
+ refreshToken: response.refresh_token,
231
+ idToken: response.id_token,
232
+ scope: response.scope,
233
+ userId
234
+ };
235
+ }
236
+ async authenticate(credentials) {
237
+ const discovery = await this.fetchDiscoveryDocument();
238
+ const grantType = credentials.grantType || "password";
239
+ const scopes = credentials.scopes || this.options.scopes || ["openid", "profile", "email"];
240
+ const body = new URLSearchParams({
241
+ grant_type: grantType,
242
+ client_id: this.options.clientId,
243
+ scope: scopes.join(" ")
244
+ });
245
+ if (grantType === "client_credentials") {
246
+ if (!this.options.clientSecret) {
247
+ throw new InvalidCredentialsError("keycloak", {
248
+ reason: "Client secret is required for client_credentials grant"
249
+ });
250
+ }
251
+ body.set("client_secret", this.options.clientSecret);
252
+ } else {
253
+ if (!credentials.username || !credentials.password) {
254
+ throw new InvalidCredentialsError("keycloak", {
255
+ reason: "Username and password are required"
256
+ });
257
+ }
258
+ body.set("username", credentials.username);
259
+ body.set("password", credentials.password);
260
+ if (this.options.clientSecret) {
261
+ body.set("client_secret", this.options.clientSecret);
262
+ }
263
+ if (credentials.mfaCode) {
264
+ body.set("totp", credentials.mfaCode);
265
+ }
266
+ }
267
+ try {
268
+ const response = await this.request(discovery.token_endpoint, {
269
+ method: "POST",
270
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
271
+ body: body.toString()
272
+ });
273
+ let userId = "";
274
+ if (response.id_token) {
275
+ const payload = decodeJwt(response.id_token);
276
+ userId = payload.sub || "";
277
+ } else if (response.access_token) {
278
+ const payload = decodeJwt(response.access_token);
279
+ userId = payload.sub || "";
280
+ }
281
+ return {
282
+ accessToken: response.access_token,
283
+ tokenType: response.token_type || "Bearer",
284
+ expiresIn: response.expires_in,
285
+ refreshToken: response.refresh_token,
286
+ idToken: response.id_token,
287
+ scope: response.scope,
288
+ userId
289
+ };
290
+ } catch (error) {
291
+ if (error instanceof ProviderError && error.message.includes("invalid_grant") && error.message.includes("totp")) {
292
+ throw new MfaRequiredError("keycloak", ["totp"]);
293
+ }
294
+ throw error;
295
+ }
296
+ }
297
+ async refresh(refreshToken) {
298
+ const discovery = await this.fetchDiscoveryDocument();
299
+ const body = new URLSearchParams({
300
+ grant_type: "refresh_token",
301
+ client_id: this.options.clientId,
302
+ refresh_token: refreshToken
303
+ });
304
+ if (this.options.clientSecret) {
305
+ body.set("client_secret", this.options.clientSecret);
306
+ }
307
+ const response = await this.request(discovery.token_endpoint, {
308
+ method: "POST",
309
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
310
+ body: body.toString()
311
+ });
312
+ let userId = "";
313
+ if (response.access_token) {
314
+ const payload = decodeJwt(response.access_token);
315
+ userId = payload.sub || "";
316
+ }
317
+ return {
318
+ accessToken: response.access_token,
319
+ tokenType: response.token_type || "Bearer",
320
+ expiresIn: response.expires_in,
321
+ refreshToken: response.refresh_token || refreshToken,
322
+ idToken: response.id_token,
323
+ scope: response.scope,
324
+ userId
325
+ };
326
+ }
327
+ async logout(options) {
328
+ const discovery = await this.fetchDiscoveryDocument();
329
+ if (!discovery.end_session_endpoint) {
330
+ if (options?.refreshToken && discovery.revocation_endpoint) {
331
+ await this.request(discovery.revocation_endpoint, {
332
+ method: "POST",
333
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
334
+ body: new URLSearchParams({
335
+ client_id: this.options.clientId,
336
+ token: options.refreshToken,
337
+ token_type_hint: "refresh_token"
338
+ }).toString()
339
+ });
340
+ }
341
+ return;
342
+ }
343
+ const params = new URLSearchParams({
344
+ client_id: this.options.clientId
345
+ });
346
+ if (options?.token) {
347
+ params.set("id_token_hint", options.token);
348
+ }
349
+ if (options?.postLogoutRedirectUri) {
350
+ params.set("post_logout_redirect_uri", options.postLogoutRedirectUri);
351
+ }
352
+ if (options?.refreshToken && discovery.revocation_endpoint) {
353
+ const revokeParams = new URLSearchParams({
354
+ client_id: this.options.clientId,
355
+ token: options.refreshToken,
356
+ token_type_hint: "refresh_token"
357
+ });
358
+ if (this.options.clientSecret) {
359
+ revokeParams.set("client_secret", this.options.clientSecret);
360
+ }
361
+ await this.request(discovery.revocation_endpoint, {
362
+ method: "POST",
363
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
364
+ body: revokeParams.toString()
365
+ });
366
+ }
367
+ }
368
+ // ---------------------------------------------------------------------------
369
+ // TOKEN OPERATIONS
370
+ // ---------------------------------------------------------------------------
371
+ async validateToken(token, options) {
372
+ try {
373
+ const jwks = await this.getJWKS();
374
+ const discovery = await this.fetchDiscoveryDocument();
375
+ const verifyOptions = {
376
+ issuer: options?.issuer || discovery.issuer,
377
+ clockTolerance: options?.clockTolerance || 0
378
+ };
379
+ if (options?.audience) {
380
+ verifyOptions.audience = options.audience;
381
+ }
382
+ const { payload } = await jwtVerify(token, jwks, verifyOptions);
383
+ if (options?.nonce && payload.nonce !== options.nonce) {
384
+ throw new InvalidNonceError("keycloak");
385
+ }
386
+ const roles = [];
387
+ if (payload.realm_access && typeof payload.realm_access === "object") {
388
+ const realmAccess = payload.realm_access;
389
+ if (realmAccess.roles) {
390
+ roles.push(...realmAccess.roles);
391
+ }
392
+ }
393
+ if (payload.resource_access && typeof payload.resource_access === "object") {
394
+ const resourceAccess = payload.resource_access;
395
+ for (const client of Object.values(resourceAccess)) {
396
+ if (client.roles) {
397
+ roles.push(...client.roles);
398
+ }
399
+ }
400
+ }
401
+ return {
402
+ sub: payload.sub || "",
403
+ iss: payload.iss || "",
404
+ aud: payload.aud || "",
405
+ exp: payload.exp || 0,
406
+ iat: payload.iat || 0,
407
+ nbf: payload.nbf,
408
+ azp: payload.azp,
409
+ email: payload.email,
410
+ email_verified: payload.email_verified,
411
+ preferred_username: payload.preferred_username,
412
+ name: payload.name,
413
+ roles,
414
+ ...payload
415
+ };
416
+ } catch (error) {
417
+ if (error instanceof JWTExpired) {
418
+ throw new TokenExpiredError("keycloak");
419
+ }
420
+ if (error instanceof JWTClaimValidationFailed) {
421
+ return null;
422
+ }
423
+ if (error instanceof JWSSignatureVerificationFailed) {
424
+ throw new InvalidTokenError("Invalid token signature", "keycloak");
425
+ }
426
+ if (error instanceof InvalidNonceError) {
427
+ throw error;
428
+ }
429
+ return null;
430
+ }
431
+ }
432
+ decodeToken(token) {
433
+ try {
434
+ const parts = token.split(".");
435
+ if (parts.length !== 3) {
436
+ throw new InvalidTokenError("Invalid JWT format", "keycloak");
437
+ }
438
+ const header = JSON.parse(atob(parts[0]));
439
+ const payload = decodeJwt(token);
440
+ return {
441
+ header: {
442
+ alg: header.alg,
443
+ typ: header.typ,
444
+ kid: header.kid
445
+ },
446
+ payload,
447
+ signature: parts[2]
448
+ };
449
+ } catch {
450
+ throw new InvalidTokenError("Failed to decode token", "keycloak");
451
+ }
452
+ }
453
+ async introspectToken(token) {
454
+ const discovery = await this.fetchDiscoveryDocument();
455
+ if (!discovery.introspection_endpoint) {
456
+ const claims = await this.validateToken(token);
457
+ return {
458
+ active: claims !== null,
459
+ claims: claims || void 0
460
+ };
461
+ }
462
+ const body = new URLSearchParams({
463
+ client_id: this.options.clientId,
464
+ token
465
+ });
466
+ if (this.options.clientSecret) {
467
+ body.set("client_secret", this.options.clientSecret);
468
+ }
469
+ const response = await this.request(discovery.introspection_endpoint, {
470
+ method: "POST",
471
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
472
+ body: body.toString()
473
+ });
474
+ if (!response.active) {
475
+ return { active: false };
476
+ }
477
+ return {
478
+ active: true,
479
+ claims: {
480
+ sub: response.sub || "",
481
+ iss: response.iss || "",
482
+ aud: response.aud || "",
483
+ exp: response.exp || 0,
484
+ iat: response.iat || 0,
485
+ ...response
486
+ },
487
+ tokenType: response.token_type,
488
+ clientId: response.client_id,
489
+ scope: response.scope
490
+ };
491
+ }
492
+ // ---------------------------------------------------------------------------
493
+ // USER OPERATIONS
494
+ // ---------------------------------------------------------------------------
495
+ async getProfile(tokenOrSession) {
496
+ const discovery = await this.fetchDiscoveryDocument();
497
+ const response = await this.request(discovery.userinfo_endpoint, {
498
+ method: "GET",
499
+ headers: { Authorization: `Bearer ${tokenOrSession}` }
500
+ });
501
+ return {
502
+ id: response.sub,
503
+ username: response.preferred_username,
504
+ email: response.email,
505
+ emailVerified: response.email_verified,
506
+ firstName: response.given_name,
507
+ lastName: response.family_name,
508
+ displayName: response.name,
509
+ picture: response.picture
510
+ };
511
+ }
512
+ async updateProfile(tokenOrSession, profile) {
513
+ const payload = decodeJwt(tokenOrSession);
514
+ const userId = payload.sub;
515
+ if (!userId) {
516
+ throw new InvalidTokenError("Token does not contain user ID", "keycloak");
517
+ }
518
+ const accountUrl = `${this.getRealmUrl()}/account`;
519
+ const updateData = {};
520
+ if (profile.firstName !== void 0)
521
+ updateData.firstName = profile.firstName;
522
+ if (profile.lastName !== void 0) updateData.lastName = profile.lastName;
523
+ if (profile.email !== void 0) updateData.email = profile.email;
524
+ await this.request(accountUrl, {
525
+ method: "POST",
526
+ headers: { Authorization: `Bearer ${tokenOrSession}` },
527
+ body: JSON.stringify(updateData)
528
+ });
529
+ return this.getProfile(tokenOrSession);
530
+ }
531
+ async getUser(userId, adminToken) {
532
+ if (!adminToken) {
533
+ throw new AccessDeniedError("Admin token required", "keycloak");
534
+ }
535
+ const response = await this.request(
536
+ `${this.getAdminUrl()}/users/${userId}`,
537
+ { method: "GET" },
538
+ adminToken
539
+ );
540
+ return this.mapKeycloakUser(response);
541
+ }
542
+ async createUser(user, adminToken) {
543
+ const keycloakUser = {
544
+ username: user.username,
545
+ email: user.email,
546
+ firstName: user.firstName,
547
+ lastName: user.lastName,
548
+ enabled: user.enabled ?? true,
549
+ emailVerified: user.emailVerified ?? false,
550
+ attributes: user.attributes
551
+ };
552
+ if (user.password) {
553
+ keycloakUser.credentials = [
554
+ {
555
+ type: "password",
556
+ value: user.password,
557
+ temporary: false
558
+ }
559
+ ];
560
+ }
561
+ const response = await fetch(`${this.getAdminUrl()}/users`, {
562
+ method: "POST",
563
+ headers: {
564
+ "Content-Type": "application/json",
565
+ Authorization: `Bearer ${adminToken}`
566
+ },
567
+ body: JSON.stringify(keycloakUser)
568
+ });
569
+ if (!response.ok) {
570
+ const errorBody = await response.text();
571
+ if (response.status === 409) {
572
+ throw new UserAlreadyExistsError(user.username, "keycloak");
573
+ }
574
+ throw new ProviderError(
575
+ `Failed to create user: ${errorBody}`,
576
+ "keycloak"
577
+ );
578
+ }
579
+ const location = response.headers.get("Location");
580
+ if (!location) {
581
+ throw new ProviderError("Failed to get created user ID", "keycloak");
582
+ }
583
+ const userId = location.split("/").pop();
584
+ if (user.roles?.length) {
585
+ await this.assignRoles(userId, user.roles, adminToken);
586
+ }
587
+ if (user.groups?.length) {
588
+ for (const groupName of user.groups) {
589
+ await this.addUserToGroup(userId, groupName, adminToken);
590
+ }
591
+ }
592
+ return this.getUser(userId, adminToken);
593
+ }
594
+ async updateUser(userId, updates, adminToken) {
595
+ const keycloakUser = {};
596
+ if (updates.username !== void 0)
597
+ keycloakUser.username = updates.username;
598
+ if (updates.email !== void 0) keycloakUser.email = updates.email;
599
+ if (updates.firstName !== void 0)
600
+ keycloakUser.firstName = updates.firstName;
601
+ if (updates.lastName !== void 0)
602
+ keycloakUser.lastName = updates.lastName;
603
+ if (updates.enabled !== void 0) keycloakUser.enabled = updates.enabled;
604
+ if (updates.emailVerified !== void 0)
605
+ keycloakUser.emailVerified = updates.emailVerified;
606
+ if (updates.attributes !== void 0) {
607
+ keycloakUser.attributes = updates.attributes;
608
+ }
609
+ await this.request(
610
+ `${this.getAdminUrl()}/users/${userId}`,
611
+ {
612
+ method: "PUT",
613
+ body: JSON.stringify(keycloakUser)
614
+ },
615
+ adminToken
616
+ );
617
+ if (updates.password) {
618
+ await this.request(
619
+ `${this.getAdminUrl()}/users/${userId}/reset-password`,
620
+ {
621
+ method: "PUT",
622
+ body: JSON.stringify({
623
+ type: "password",
624
+ value: updates.password,
625
+ temporary: false
626
+ })
627
+ },
628
+ adminToken
629
+ );
630
+ }
631
+ return this.getUser(userId, adminToken);
632
+ }
633
+ async deleteUser(userId, adminToken) {
634
+ await this.request(
635
+ `${this.getAdminUrl()}/users/${userId}`,
636
+ { method: "DELETE" },
637
+ adminToken
638
+ );
639
+ }
640
+ async listUsers(query, adminToken) {
641
+ if (!adminToken) {
642
+ throw new AccessDeniedError("Admin token required", "keycloak");
643
+ }
644
+ const params = new URLSearchParams();
645
+ if (query.search) params.set("search", query.search);
646
+ if (query.email) params.set("email", query.email);
647
+ if (query.username) params.set("username", query.username);
648
+ if (query.enabled !== void 0)
649
+ params.set("enabled", String(query.enabled));
650
+ if (query.limit) params.set("max", String(query.limit));
651
+ if (query.offset) params.set("first", String(query.offset));
652
+ const users = await this.request(
653
+ `${this.getAdminUrl()}/users?${params.toString()}`,
654
+ { method: "GET" },
655
+ adminToken
656
+ );
657
+ const countResponse = await this.request(
658
+ `${this.getAdminUrl()}/users/count?${params.toString()}`,
659
+ { method: "GET" },
660
+ adminToken
661
+ );
662
+ return {
663
+ users: users.map((u) => this.mapKeycloakUser(u)),
664
+ total: countResponse,
665
+ limit: query.limit || 100,
666
+ offset: query.offset || 0
667
+ };
668
+ }
669
+ async requestPasswordReset(email) {
670
+ throw new NotImplementedError("requestPasswordReset", "keycloak", {
671
+ reason: "Requires admin token or use Keycloak login page for self-service reset"
672
+ });
673
+ }
674
+ async resetPassword(token, newPassword) {
675
+ throw new NotImplementedError("resetPassword", "keycloak", {
676
+ reason: "Password reset is handled by Keycloak login flow"
677
+ });
678
+ }
679
+ // ---------------------------------------------------------------------------
680
+ // SESSION OPERATIONS
681
+ // ---------------------------------------------------------------------------
682
+ async listSessions(userId, adminToken) {
683
+ if (!adminToken) {
684
+ throw new AccessDeniedError("Admin token required", "keycloak");
685
+ }
686
+ const sessions = await this.request(
687
+ `${this.getAdminUrl()}/users/${userId}/sessions`,
688
+ { method: "GET" },
689
+ adminToken
690
+ );
691
+ return sessions.map((s) => ({
692
+ id: s.id,
693
+ userId: s.userId,
694
+ clientId: s.clients ? Object.keys(s.clients).join(", ") : void 0,
695
+ startedAt: new Date(s.start),
696
+ lastAccessedAt: new Date(s.lastAccess),
697
+ ipAddress: s.ipAddress,
698
+ userAgent: s.userAgent
699
+ }));
700
+ }
701
+ async revokeSession(sessionId, adminToken) {
702
+ if (!adminToken) {
703
+ throw new AccessDeniedError("Admin token required", "keycloak");
704
+ }
705
+ await this.request(
706
+ `${this.getAdminUrl()}/sessions/${sessionId}`,
707
+ { method: "DELETE" },
708
+ adminToken
709
+ );
710
+ }
711
+ async revokeAllSessions(userId, adminToken) {
712
+ if (!adminToken) {
713
+ throw new AccessDeniedError("Admin token required", "keycloak");
714
+ }
715
+ await this.request(
716
+ `${this.getAdminUrl()}/users/${userId}/logout`,
717
+ { method: "POST" },
718
+ adminToken
719
+ );
720
+ }
721
+ // ---------------------------------------------------------------------------
722
+ // AUTHORIZATION
723
+ // ---------------------------------------------------------------------------
724
+ async hasRole(tokenOrUserId, role) {
725
+ const roles = await this.getRoles(tokenOrUserId);
726
+ return roles.includes(role);
727
+ }
728
+ async hasPermission(tokenOrUserId, permission, resource) {
729
+ const roles = await this.getRoles(tokenOrUserId);
730
+ const permissionRole = resource ? `${resource}:${permission}` : permission;
731
+ return roles.includes(permissionRole) || roles.includes(permission);
732
+ }
733
+ async getRoles(tokenOrUserId, adminToken) {
734
+ try {
735
+ const claims = await this.validateToken(tokenOrUserId);
736
+ if (claims) {
737
+ return claims.roles || [];
738
+ }
739
+ } catch {
740
+ }
741
+ if (adminToken) {
742
+ try {
743
+ const roleMappings = await this.request(
744
+ `${this.getAdminUrl()}/users/${tokenOrUserId}/role-mappings`,
745
+ { method: "GET" },
746
+ adminToken
747
+ );
748
+ return roleMappings.realmMappings?.map((r) => r.name) || [];
749
+ } catch {
750
+ }
751
+ }
752
+ return [];
753
+ }
754
+ async assignRole(userId, role, adminToken) {
755
+ const roles = await this.request(
756
+ `${this.getAdminUrl()}/roles`,
757
+ { method: "GET" },
758
+ adminToken
759
+ );
760
+ const roleObj = roles.find((r) => r.name === role);
761
+ if (!roleObj) {
762
+ throw new ProviderError(`Role not found: ${role}`, "keycloak");
763
+ }
764
+ await this.request(
765
+ `${this.getAdminUrl()}/users/${userId}/role-mappings/realm`,
766
+ {
767
+ method: "POST",
768
+ body: JSON.stringify([roleObj])
769
+ },
770
+ adminToken
771
+ );
772
+ }
773
+ async removeRole(userId, role, adminToken) {
774
+ const roles = await this.request(
775
+ `${this.getAdminUrl()}/roles`,
776
+ { method: "GET" },
777
+ adminToken
778
+ );
779
+ const roleObj = roles.find((r) => r.name === role);
780
+ if (!roleObj) {
781
+ throw new ProviderError(`Role not found: ${role}`, "keycloak");
782
+ }
783
+ await this.request(
784
+ `${this.getAdminUrl()}/users/${userId}/role-mappings/realm`,
785
+ {
786
+ method: "DELETE",
787
+ body: JSON.stringify([roleObj])
788
+ },
789
+ adminToken
790
+ );
791
+ }
792
+ // ---------------------------------------------------------------------------
793
+ // PROVIDER INFORMATION
794
+ // ---------------------------------------------------------------------------
795
+ async getCapabilities() {
796
+ return {
797
+ authorizationCode: true,
798
+ passwordGrant: true,
799
+ clientCredentials: true,
800
+ tokenRefresh: true,
801
+ oidc: true,
802
+ userManagement: true,
803
+ sessionManagement: true,
804
+ rbac: true,
805
+ passwordReset: true,
806
+ mfa: true,
807
+ socialLogin: true,
808
+ federation: true,
809
+ decentralized: false
810
+ };
811
+ }
812
+ async getDiscoveryDocument() {
813
+ return this.fetchDiscoveryDocument();
814
+ }
815
+ // ---------------------------------------------------------------------------
816
+ // PRIVATE HELPERS
817
+ // ---------------------------------------------------------------------------
818
+ mapKeycloakUser(user) {
819
+ return {
820
+ id: user.id,
821
+ username: user.username,
822
+ email: user.email,
823
+ emailVerified: user.emailVerified,
824
+ firstName: user.firstName,
825
+ lastName: user.lastName,
826
+ displayName: user.firstName && user.lastName ? `${user.firstName} ${user.lastName}` : user.username,
827
+ enabled: user.enabled,
828
+ createdAt: user.createdTimestamp ? new Date(user.createdTimestamp) : void 0,
829
+ attributes: user.attributes,
830
+ groups: user.groups
831
+ };
832
+ }
833
+ async assignRoles(userId, roleNames, adminToken) {
834
+ const allRoles = await this.request(
835
+ `${this.getAdminUrl()}/roles`,
836
+ { method: "GET" },
837
+ adminToken
838
+ );
839
+ const rolesToAssign = allRoles.filter((r) => roleNames.includes(r.name));
840
+ if (rolesToAssign.length > 0) {
841
+ await this.request(
842
+ `${this.getAdminUrl()}/users/${userId}/role-mappings/realm`,
843
+ {
844
+ method: "POST",
845
+ body: JSON.stringify(rolesToAssign)
846
+ },
847
+ adminToken
848
+ );
849
+ }
850
+ }
851
+ async addUserToGroup(userId, groupName, adminToken) {
852
+ const groups = await this.request(
853
+ `${this.getAdminUrl()}/groups?search=${encodeURIComponent(groupName)}`,
854
+ { method: "GET" },
855
+ adminToken
856
+ );
857
+ const group = groups.find((g) => g.name === groupName);
858
+ if (!group) {
859
+ throw new ProviderError(`Group not found: ${groupName}`, "keycloak");
860
+ }
861
+ await this.request(
862
+ `${this.getAdminUrl()}/users/${userId}/groups/${group.id}`,
863
+ { method: "PUT" },
864
+ adminToken
865
+ );
866
+ }
867
+ }
868
+ export {
869
+ KeycloakProvider
870
+ };
871
+ //# sourceMappingURL=keycloak-t6JEUeOz.js.map