@web-ts-toolkit/moo 0.39.0 → 0.40.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 CHANGED
@@ -101,16 +101,12 @@ pnpm add @egose/keycloak-fluent
101
101
  ```
102
102
 
103
103
  ```ts
104
- import KeycloakAdminClientFluent from '@egose/keycloak-fluent';
105
104
  import { Schema } from 'mongoose';
106
- import { keycloakUserSyncPlugin } from '@web-ts-toolkit/moo/plugins/keycloak-user-sync';
105
+ import { createManagedKeycloakClient, keycloakUserSyncPlugin } from '@web-ts-toolkit/moo/plugins/keycloak-user-sync';
107
106
 
108
- const keycloak = new KeycloakAdminClientFluent({
107
+ const keycloak = createManagedKeycloakClient({
109
108
  baseUrl: process.env.KEYCLOAK_URL,
110
- realmName: 'master',
111
- });
112
-
113
- await keycloak.simpleAuth({
109
+ authRealm: 'master',
114
110
  clientId: process.env.KEYCLOAK_CLIENT_ID,
115
111
  clientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
116
112
  });
@@ -161,6 +157,8 @@ userSchema.plugin(keycloakUserSyncPlugin, {
161
157
  });
162
158
  ```
163
159
 
160
+ `createManagedKeycloakClient` is provided by `@egose/keycloak-fluent` and re-exported here for convenience. It authenticates lazily, shares one authentication attempt across concurrent requests, and creates no timer, so the same client can be declared outside a serverless handler and reused by warm invocations. The default mode is `client_credentials`; pass `authMode: 'user_credentials'` with lazy `username` and `password` resolvers when direct user credentials are required. Service accounts remain recommended for background synchronization.
161
+
164
162
  The plugin:
165
163
 
166
164
  - creates or updates a Keycloak user after a document `save()`
package/llms.txt CHANGED
@@ -64,10 +64,19 @@ userSchema.plugin(newDocumentPlugin, {
64
64
  Keycloak user-sync plugin:
65
65
 
66
66
  ```ts
67
- import { keycloakUserSyncPlugin } from '@web-ts-toolkit/moo/plugins/keycloak-user-sync';
67
+ import {
68
+ createManagedKeycloakClient,
69
+ keycloakUserSyncPlugin,
70
+ } from '@web-ts-toolkit/moo/plugins/keycloak-user-sync';
71
+
72
+ const keycloak = createManagedKeycloakClient({
73
+ baseUrl: process.env.KEYCLOAK_URL!,
74
+ clientId: process.env.KEYCLOAK_CLIENT_ID!,
75
+ clientSecret: process.env.KEYCLOAK_CLIENT_SECRET!,
76
+ });
68
77
 
69
78
  userSchema.plugin(keycloakUserSyncPlugin, {
70
- client: authenticatedKeycloakFluentClient,
79
+ client: keycloak,
71
80
  realm: 'application',
72
81
  identifyBy: ['providerId', 'username', 'email'],
73
82
  managedRoles: ['admin', 'editor', 'viewer'],
@@ -91,6 +100,7 @@ userSchema.plugin(keycloakUserSyncPlugin, {
91
100
  - optional peer dependency: `@egose/keycloak-fluent ^0.7` only for `@web-ts-toolkit/moo/plugins/keycloak-user-sync`
92
101
  - root entrypoint exports schema helpers, `isObjectId`, and non-Keycloak plugins; dedicated plugin subpaths are also available
93
102
  - Keycloak user sync is not re-exported from root or `@web-ts-toolkit/moo/plugins`; import it from the direct subpath
103
+ - `createManagedKeycloakClient` lazily uses `client_credentials`, renews on demand without timers, and deduplicates concurrent authentication; construct a client directly for custom grants
94
104
  - `isObjectId(...)` is a strict guard: it rejects strings that `ObjectId.isValid` accepts but that don't round-trip back to the same string
95
105
  - `uniqueNullableString`/`uniqueEmptiableString` return partial-index field definitions, not full schemas
96
106
  - new-document plugin runs its callback only after the first successful `save()` of a newly inserted document
@@ -112,7 +122,7 @@ userSchema.plugin(keycloakUserSyncPlugin, {
112
122
  - `@web-ts-toolkit/moo/plugins/cascade-delete` — `cascadeDeletePlugin`
113
123
  - `@web-ts-toolkit/moo/plugins/model-function` — `modelFunctionPlugin`
114
124
  - `@web-ts-toolkit/moo/plugins/new-document` — `newDocumentPlugin`
115
- - `@web-ts-toolkit/moo/plugins/keycloak-user-sync` — `keycloakUserSyncPlugin`
125
+ - `@web-ts-toolkit/moo/plugins/keycloak-user-sync` — `createManagedKeycloakClient`, `keycloakUserSyncPlugin`
116
126
 
117
127
  ## Pointers
118
128
 
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@web-ts-toolkit/moo",
3
3
  "description": "Mongoose helpers for schema fields, ObjectId checks, and document plugins",
4
4
  "homepage": "https://web-ts-toolkit.pages.dev/docs/packages/moo",
5
- "version": "0.39.0",
5
+ "version": "0.40.1",
6
6
  "sideEffects": false,
7
7
  "keywords": [
8
8
  "mongoose",
@@ -74,7 +74,7 @@
74
74
  "node": ">=22"
75
75
  },
76
76
  "peerDependencies": {
77
- "@egose/keycloak-fluent": "^0.7.0",
77
+ "@egose/keycloak-fluent": "^0.12.1",
78
78
  "mongoose": ">=8.0.0"
79
79
  },
80
80
  "peerDependenciesMeta": {
@@ -83,7 +83,7 @@
83
83
  }
84
84
  },
85
85
  "dependencies": {
86
- "@web-ts-toolkit/utils": "0.39.0"
86
+ "@web-ts-toolkit/utils": "0.40.1"
87
87
  },
88
88
  "files": [
89
89
  "**/*",
@@ -1,6 +1,8 @@
1
- import KeycloakAdminClientFluent from '@egose/keycloak-fluent';
1
+ import KeycloakAdminClientFluent, { ManagedKeycloakCredential } from '@egose/keycloak-fluent';
2
+ export { ManagedKeycloakClientOptions, createManagedKeycloakClient } from '@egose/keycloak-fluent';
2
3
  import { Schema } from 'mongoose';
3
4
 
5
+ type ManagedKeycloakClientSecret = ManagedKeycloakCredential;
4
6
  type KeycloakUserIdentityField = 'providerId' | 'username' | 'email';
5
7
  type KeycloakUserSyncField = 'username' | 'email' | 'emailVerified' | 'firstName' | 'lastName' | 'enabled' | 'roles' | 'attributes' | 'password';
6
8
  interface KeycloakUserSyncPaths {
@@ -32,7 +34,7 @@ interface KeycloakUserSyncDocument {
32
34
  get(path: string): unknown;
33
35
  }
34
36
  interface KeycloakUserSyncPluginOptions {
35
- /** An authenticated client. Authentication and token refresh remain the application's responsibility. */
37
+ /** A client that can provide admin access tokens. Use createManagedKeycloakClient for lazy managed authentication. */
36
38
  client: KeycloakAdminClientFluent;
37
39
  realm: string;
38
40
  /**
@@ -85,4 +87,4 @@ interface KeycloakUserSyncPluginOptions {
85
87
  */
86
88
  declare function keycloakUserSyncPlugin(schema: Schema, rawOptions: KeycloakUserSyncPluginOptions): void;
87
89
 
88
- export { type KeycloakUserIdentityField, type KeycloakUserSyncDocument, type KeycloakUserSyncErrorContext, type KeycloakUserSyncField, type KeycloakUserSyncLogger, type KeycloakUserSyncPaths, type KeycloakUserSyncPluginOptions, type KeycloakUserSyncSensitiveErrorContext, keycloakUserSyncPlugin };
90
+ export { type KeycloakUserIdentityField, type KeycloakUserSyncDocument, type KeycloakUserSyncErrorContext, type KeycloakUserSyncField, type KeycloakUserSyncLogger, type KeycloakUserSyncPaths, type KeycloakUserSyncPluginOptions, type KeycloakUserSyncSensitiveErrorContext, type ManagedKeycloakClientSecret, keycloakUserSyncPlugin };
@@ -1,6 +1,8 @@
1
- import KeycloakAdminClientFluent from '@egose/keycloak-fluent';
1
+ import KeycloakAdminClientFluent, { ManagedKeycloakCredential } from '@egose/keycloak-fluent';
2
+ export { ManagedKeycloakClientOptions, createManagedKeycloakClient } from '@egose/keycloak-fluent';
2
3
  import { Schema } from 'mongoose';
3
4
 
5
+ type ManagedKeycloakClientSecret = ManagedKeycloakCredential;
4
6
  type KeycloakUserIdentityField = 'providerId' | 'username' | 'email';
5
7
  type KeycloakUserSyncField = 'username' | 'email' | 'emailVerified' | 'firstName' | 'lastName' | 'enabled' | 'roles' | 'attributes' | 'password';
6
8
  interface KeycloakUserSyncPaths {
@@ -32,7 +34,7 @@ interface KeycloakUserSyncDocument {
32
34
  get(path: string): unknown;
33
35
  }
34
36
  interface KeycloakUserSyncPluginOptions {
35
- /** An authenticated client. Authentication and token refresh remain the application's responsibility. */
37
+ /** A client that can provide admin access tokens. Use createManagedKeycloakClient for lazy managed authentication. */
36
38
  client: KeycloakAdminClientFluent;
37
39
  realm: string;
38
40
  /**
@@ -85,4 +87,4 @@ interface KeycloakUserSyncPluginOptions {
85
87
  */
86
88
  declare function keycloakUserSyncPlugin(schema: Schema, rawOptions: KeycloakUserSyncPluginOptions): void;
87
89
 
88
- export { type KeycloakUserIdentityField, type KeycloakUserSyncDocument, type KeycloakUserSyncErrorContext, type KeycloakUserSyncField, type KeycloakUserSyncLogger, type KeycloakUserSyncPaths, type KeycloakUserSyncPluginOptions, type KeycloakUserSyncSensitiveErrorContext, keycloakUserSyncPlugin };
90
+ export { type KeycloakUserIdentityField, type KeycloakUserSyncDocument, type KeycloakUserSyncErrorContext, type KeycloakUserSyncField, type KeycloakUserSyncLogger, type KeycloakUserSyncPaths, type KeycloakUserSyncPluginOptions, type KeycloakUserSyncSensitiveErrorContext, type ManagedKeycloakClientSecret, keycloakUserSyncPlugin };
@@ -20,60 +20,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/plugins/keycloak-user-sync.ts
21
21
  var keycloak_user_sync_exports = {};
22
22
  __export(keycloak_user_sync_exports, {
23
+ createManagedKeycloakClient: () => import_keycloak_fluent.createManagedKeycloakClient,
23
24
  keycloakUserSyncPlugin: () => keycloakUserSyncPlugin
24
25
  });
25
26
  module.exports = __toCommonJS(keycloak_user_sync_exports);
26
-
27
- // src/plugins/keycloak-user-sync/adapter.ts
28
- var createKeycloakUserSyncAdapter = (client, realm) => {
29
- const supportedClient = client;
30
- const realmHandle = () => supportedClient.realm(realm);
31
- const users = supportedClient.core.users;
32
- return {
33
- async getRealm() {
34
- return realmHandle().get();
35
- },
36
- async getUserById(username, id) {
37
- return realmHandle().user(username).getById(id);
38
- },
39
- async getUserByUsername(username) {
40
- return realmHandle().user(username).get();
41
- },
42
- async findUsersByEmail(email) {
43
- return users.find({ realm, email, exact: true, first: 0, max: 2 });
44
- },
45
- async createUser(username, payload) {
46
- return realmHandle().user(username).create(payload);
47
- },
48
- async updateUser(id, payload) {
49
- await users.update({ realm, id }, payload);
50
- },
51
- async deleteUser(id) {
52
- await users.del({ realm, id });
53
- },
54
- async sendVerifyEmail(id) {
55
- await users.sendVerifyEmail({ realm, id });
56
- },
57
- async resetPassword(id, value, temporary) {
58
- await users.resetPassword({ realm, id, credential: { temporary, type: "password", value } });
59
- },
60
- async ensureRole(name) {
61
- await realmHandle().role(name).ensure({});
62
- },
63
- async getRole(name) {
64
- return realmHandle().role(name).get();
65
- },
66
- async listRealmRoleMappings(id) {
67
- return users.listRealmRoleMappings({ realm, id });
68
- },
69
- async addRealmRoleMappings(id, roles) {
70
- await users.addRealmRoleMappings({ realm, id, roles });
71
- },
72
- async delRealmRoleMappings(id, roles) {
73
- await users.delRealmRoleMappings({ realm, id, roles });
74
- }
75
- };
76
- };
27
+ var import_keycloak_fluent = require("@egose/keycloak-fluent");
77
28
 
78
29
  // src/plugins/keycloak-user-sync/planner.ts
79
30
  var dangerousAttributeKeys = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
@@ -222,18 +173,6 @@ var planEmailVerification = ({
222
173
  initialLinkSameEmail
223
174
  };
224
175
  };
225
- var planRoleDiff = (desiredNames, assignedRoles, desiredRoles, managedRoles) => {
226
- const managedNames = managedRoles ? new Set(managedRoles) : null;
227
- const effectiveDesiredNames = managedNames ? desiredNames.filter((name) => managedNames.has(name)) : desiredNames;
228
- const assignedNames = new Set(assignedRoles.map((role) => role.name).filter((name) => Boolean(name)));
229
- const desiredNameSet = new Set(effectiveDesiredNames);
230
- return {
231
- desiredNames,
232
- effectiveDesiredNames,
233
- toAdd: desiredRoles.filter((role) => role.name && !assignedNames.has(role.name)),
234
- toRemove: managedNames ? assignedRoles.filter((role) => role.name && !desiredNameSet.has(role.name) && managedNames.has(role.name)) : []
235
- };
236
- };
237
176
 
238
177
  // src/plugins/keycloak-user-sync.ts
239
178
  var syncStateKey = "keycloakUserSync";
@@ -413,7 +352,7 @@ function keycloakUserSyncPlugin(schema, rawOptions) {
413
352
  const paths = options.paths;
414
353
  const syncFields = options.syncFields;
415
354
  const identities = options.identifyBy;
416
- const adapter = createKeycloakUserSyncAdapter(options.client, options.realm);
355
+ const realmHandle = options.client.realm(options.realm);
417
356
  const trackedPaths = buildTrackedPaths(options);
418
357
  const buildSafeErrorContext = (operation, document) => ({
419
358
  operation,
@@ -437,13 +376,13 @@ function keycloakUserSyncPlugin(schema, rawOptions) {
437
376
  const getPreviousValue = (previous, field) => getPathValue(previous, paths[field]);
438
377
  const getUsername = (document) => stringValue(readDocumentValue(document, "username")) ?? stringValue(readDocumentValue(document, "email"));
439
378
  const resolveUser = async (document, previous) => {
440
- const realm = await adapter.getRealm();
379
+ const realm = await realmHandle.get();
441
380
  if (!realm) throw new Error(`Keycloak realm "${options.realm}" was not found`);
442
381
  const duplicateEmailsAllowed = realm.duplicateEmailsAllowed === true;
443
382
  const orderedIdentities = duplicateEmailsAllowed ? ["providerId", "username", "email"].filter((identity) => identities.includes(identity)) : identities;
444
383
  const getCompleteUserById = async (user) => {
445
384
  if (!user.id) return user;
446
- return await adapter.getUserById(user.username ?? user.id, user.id) ?? user;
385
+ return await realmHandle.userById(user.id).get() ?? user;
447
386
  };
448
387
  for (const identity of orderedIdentities) {
449
388
  if (identity === "providerId") {
@@ -451,7 +390,7 @@ function keycloakUserSyncPlugin(schema, rawOptions) {
451
390
  readDocumentValue(document, "providerId"),
452
391
  getPreviousValue(previous, "providerId")
453
392
  ])) {
454
- const user = await adapter.getUserById(getUsername(document) ?? providerId, providerId);
393
+ const user = await realmHandle.userById(providerId).get();
455
394
  if (user) return { user, duplicateEmailsAllowed };
456
395
  }
457
396
  }
@@ -460,7 +399,7 @@ function keycloakUserSyncPlugin(schema, rawOptions) {
460
399
  readDocumentValue(document, "username"),
461
400
  getPreviousValue(previous, "username")
462
401
  ])) {
463
- const user = await adapter.getUserByUsername(username);
402
+ const user = await realmHandle.user(username).get();
464
403
  if (user) return { user, duplicateEmailsAllowed };
465
404
  }
466
405
  }
@@ -469,9 +408,7 @@ function keycloakUserSyncPlugin(schema, rawOptions) {
469
408
  readDocumentValue(document, "email"),
470
409
  getPreviousValue(previous, "email")
471
410
  ])) {
472
- const matches = (await adapter.findUsersByEmail(email)).filter(
473
- (user) => normalizeEmail(user.email) === normalizeEmail(email)
474
- );
411
+ const matches = (await realmHandle.searchUsers(email, { attribute: "email", exact: true, first: 0, max: 2 })).filter((user) => normalizeEmail(user.email) === normalizeEmail(email));
475
412
  if (matches.length > 1) {
476
413
  throw new Error("Cannot identify a unique Keycloak user for the configured email identity");
477
414
  }
@@ -482,17 +419,15 @@ function keycloakUserSyncPlugin(schema, rawOptions) {
482
419
  return { user: null, duplicateEmailsAllowed };
483
420
  };
484
421
  const resolveCreatedUser = async (document, username) => {
485
- const userByUsername = await adapter.getUserByUsername(username);
422
+ const userByUsername = await realmHandle.user(username).get();
486
423
  if (userByUsername) return userByUsername;
487
424
  const email = stringValue(readDocumentValue(document, "email"));
488
425
  if (!email) return null;
489
- const matches = (await adapter.findUsersByEmail(email)).filter(
490
- (user) => normalizeEmail(user.email) === normalizeEmail(email)
491
- );
426
+ const matches = (await realmHandle.searchUsers(email, { attribute: "email", exact: true, first: 0, max: 2 })).filter((user) => normalizeEmail(user.email) === normalizeEmail(email));
492
427
  if (matches.length > 1) throw new Error("Cannot identify a unique Keycloak user for the configured email identity");
493
428
  const match = matches[0];
494
429
  if (!match?.id) return match ?? null;
495
- return await adapter.getUserById(match.username ?? match.id, match.id) ?? match;
430
+ return await realmHandle.userById(match.id).get() ?? match;
496
431
  };
497
432
  const syncRoles = async (user, document) => {
498
433
  if (!syncFields.roles || !user.id) return;
@@ -502,24 +437,12 @@ function keycloakUserSyncPlugin(schema, rawOptions) {
502
437
  if (desiredNames.length > maxRolesPerSync) {
503
438
  throw new Error(`keycloakUserSyncPlugin supports at most ${maxRolesPerSync} desired roles per sync operation`);
504
439
  }
505
- const managedNames = options.managedRoles ? new Set(options.managedRoles) : null;
506
- const effectiveDesiredNames = managedNames ? desiredNames.filter((name) => managedNames.has(name)) : desiredNames;
507
- if (effectiveDesiredNames.length === 0 && !managedNames) return;
508
- const desiredRoles = [];
509
- for (const roleName of effectiveDesiredNames) {
510
- if (options.ensureRoles !== false) await adapter.ensureRole(roleName);
511
- const role = await adapter.getRole(roleName);
512
- if (!role?.id) throw new Error(`Keycloak realm role "${roleName}" was not found`);
513
- desiredRoles.push(role);
514
- }
515
- const assignedRoles = await adapter.listRealmRoleMappings(user.id);
516
- const { toAdd, toRemove } = planRoleDiff(desiredNames, assignedRoles, desiredRoles, options.managedRoles);
517
- if (toAdd.length) {
518
- await adapter.addRealmRoleMappings(user.id, toAdd);
519
- }
520
- if (toRemove.length) {
521
- await adapter.delRealmRoleMappings(user.id, toRemove);
522
- }
440
+ if (desiredNames.length === 0 && !options.managedRoles) return;
441
+ await realmHandle.userById(user.id).reconcileRealmRoles(desiredNames, {
442
+ ensureMissing: options.ensureRoles !== false,
443
+ managedRoleNames: options.managedRoles,
444
+ maxRoles: maxRolesPerSync
445
+ });
523
446
  };
524
447
  const persistProviderId = async (document, providerId) => {
525
448
  if (options.persistProviderId === false || readDocumentValue(document, "providerId") === providerId) return;
@@ -546,14 +469,20 @@ function keycloakUserSyncPlugin(schema, rawOptions) {
546
469
  if (!username) throw new Error("Cannot create a Keycloak user without a username or email");
547
470
  payload.attributes = mergeAttributes(null, document, options);
548
471
  try {
549
- user = await adapter.createUser(username, payload) ?? null;
472
+ user = await realmHandle.user(username).create({
473
+ ...payload,
474
+ ...desiredPassword && {
475
+ password: desiredPassword,
476
+ passwordTemporary: options.passwordTemporary ?? false
477
+ }
478
+ }) ?? null;
479
+ created = true;
550
480
  } catch (error) {
551
481
  if (identities.length === 0) throw error;
552
482
  const recoveredUser = await resolveCreatedUser(document, username);
553
483
  if (!recoveredUser) throw error;
554
484
  user = recoveredUser;
555
485
  }
556
- created = true;
557
486
  }
558
487
  if (!user?.id) throw new Error("Keycloak did not return a user ID");
559
488
  const userId = user.id;
@@ -576,21 +505,21 @@ function keycloakUserSyncPlugin(schema, rawOptions) {
576
505
  }
577
506
  if (shouldSyncAttributes) payload.attributes = mergeAttributes(user, document, options);
578
507
  if (Object.keys(payload).length > 0) {
579
- await adapter.updateUser(userId, payload);
508
+ await realmHandle.userById(userId).update(payload);
580
509
  user = { ...user, ...payload };
581
510
  }
582
511
  }
583
- if ((created || state.passwordChanged) && desiredPassword) {
584
- await adapter.resetPassword(userId, desiredPassword, options.passwordTemporary ?? false);
512
+ if (!created && state.passwordChanged && desiredPassword) {
513
+ await realmHandle.userById(userId).resetPassword(desiredPassword, { temporary: options.passwordTemporary ?? false });
585
514
  }
586
515
  if (created || state.changedFields.has("roles")) await syncRoles(user, document);
587
516
  if (emailVerificationPlan.changed && options.sendVerificationEmailOnChange !== false) {
588
- await adapter.sendVerifyEmail(userId);
517
+ await realmHandle.userById(userId).sendVerifyEmail();
589
518
  }
590
519
  };
591
520
  const deleteDocument = async (document) => {
592
521
  const { user } = await resolveUser(document);
593
- if (user?.id) await adapter.deleteUser(user.id);
522
+ if (user?.id) await realmHandle.userById(user.id).delete();
594
523
  };
595
524
  schema.pre("save", async function keycloakUserSyncPreSave() {
596
525
  assertNoDocumentSession(this, "save");
@@ -640,5 +569,6 @@ function keycloakUserSyncPlugin(schema, rawOptions) {
640
569
  }
641
570
  // Annotate the CommonJS export names for ESM import in node:
642
571
  0 && (module.exports = {
572
+ createManagedKeycloakClient,
643
573
  keycloakUserSyncPlugin
644
574
  });
@@ -1,53 +1,7 @@
1
- // src/plugins/keycloak-user-sync/adapter.ts
2
- var createKeycloakUserSyncAdapter = (client, realm) => {
3
- const supportedClient = client;
4
- const realmHandle = () => supportedClient.realm(realm);
5
- const users = supportedClient.core.users;
6
- return {
7
- async getRealm() {
8
- return realmHandle().get();
9
- },
10
- async getUserById(username, id) {
11
- return realmHandle().user(username).getById(id);
12
- },
13
- async getUserByUsername(username) {
14
- return realmHandle().user(username).get();
15
- },
16
- async findUsersByEmail(email) {
17
- return users.find({ realm, email, exact: true, first: 0, max: 2 });
18
- },
19
- async createUser(username, payload) {
20
- return realmHandle().user(username).create(payload);
21
- },
22
- async updateUser(id, payload) {
23
- await users.update({ realm, id }, payload);
24
- },
25
- async deleteUser(id) {
26
- await users.del({ realm, id });
27
- },
28
- async sendVerifyEmail(id) {
29
- await users.sendVerifyEmail({ realm, id });
30
- },
31
- async resetPassword(id, value, temporary) {
32
- await users.resetPassword({ realm, id, credential: { temporary, type: "password", value } });
33
- },
34
- async ensureRole(name) {
35
- await realmHandle().role(name).ensure({});
36
- },
37
- async getRole(name) {
38
- return realmHandle().role(name).get();
39
- },
40
- async listRealmRoleMappings(id) {
41
- return users.listRealmRoleMappings({ realm, id });
42
- },
43
- async addRealmRoleMappings(id, roles) {
44
- await users.addRealmRoleMappings({ realm, id, roles });
45
- },
46
- async delRealmRoleMappings(id, roles) {
47
- await users.delRealmRoleMappings({ realm, id, roles });
48
- }
49
- };
50
- };
1
+ // src/plugins/keycloak-user-sync.ts
2
+ import {
3
+ createManagedKeycloakClient
4
+ } from "@egose/keycloak-fluent";
51
5
 
52
6
  // src/plugins/keycloak-user-sync/planner.ts
53
7
  var dangerousAttributeKeys = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
@@ -196,18 +150,6 @@ var planEmailVerification = ({
196
150
  initialLinkSameEmail
197
151
  };
198
152
  };
199
- var planRoleDiff = (desiredNames, assignedRoles, desiredRoles, managedRoles) => {
200
- const managedNames = managedRoles ? new Set(managedRoles) : null;
201
- const effectiveDesiredNames = managedNames ? desiredNames.filter((name) => managedNames.has(name)) : desiredNames;
202
- const assignedNames = new Set(assignedRoles.map((role) => role.name).filter((name) => Boolean(name)));
203
- const desiredNameSet = new Set(effectiveDesiredNames);
204
- return {
205
- desiredNames,
206
- effectiveDesiredNames,
207
- toAdd: desiredRoles.filter((role) => role.name && !assignedNames.has(role.name)),
208
- toRemove: managedNames ? assignedRoles.filter((role) => role.name && !desiredNameSet.has(role.name) && managedNames.has(role.name)) : []
209
- };
210
- };
211
153
 
212
154
  // src/plugins/keycloak-user-sync.ts
213
155
  var syncStateKey = "keycloakUserSync";
@@ -387,7 +329,7 @@ function keycloakUserSyncPlugin(schema, rawOptions) {
387
329
  const paths = options.paths;
388
330
  const syncFields = options.syncFields;
389
331
  const identities = options.identifyBy;
390
- const adapter = createKeycloakUserSyncAdapter(options.client, options.realm);
332
+ const realmHandle = options.client.realm(options.realm);
391
333
  const trackedPaths = buildTrackedPaths(options);
392
334
  const buildSafeErrorContext = (operation, document) => ({
393
335
  operation,
@@ -411,13 +353,13 @@ function keycloakUserSyncPlugin(schema, rawOptions) {
411
353
  const getPreviousValue = (previous, field) => getPathValue(previous, paths[field]);
412
354
  const getUsername = (document) => stringValue(readDocumentValue(document, "username")) ?? stringValue(readDocumentValue(document, "email"));
413
355
  const resolveUser = async (document, previous) => {
414
- const realm = await adapter.getRealm();
356
+ const realm = await realmHandle.get();
415
357
  if (!realm) throw new Error(`Keycloak realm "${options.realm}" was not found`);
416
358
  const duplicateEmailsAllowed = realm.duplicateEmailsAllowed === true;
417
359
  const orderedIdentities = duplicateEmailsAllowed ? ["providerId", "username", "email"].filter((identity) => identities.includes(identity)) : identities;
418
360
  const getCompleteUserById = async (user) => {
419
361
  if (!user.id) return user;
420
- return await adapter.getUserById(user.username ?? user.id, user.id) ?? user;
362
+ return await realmHandle.userById(user.id).get() ?? user;
421
363
  };
422
364
  for (const identity of orderedIdentities) {
423
365
  if (identity === "providerId") {
@@ -425,7 +367,7 @@ function keycloakUserSyncPlugin(schema, rawOptions) {
425
367
  readDocumentValue(document, "providerId"),
426
368
  getPreviousValue(previous, "providerId")
427
369
  ])) {
428
- const user = await adapter.getUserById(getUsername(document) ?? providerId, providerId);
370
+ const user = await realmHandle.userById(providerId).get();
429
371
  if (user) return { user, duplicateEmailsAllowed };
430
372
  }
431
373
  }
@@ -434,7 +376,7 @@ function keycloakUserSyncPlugin(schema, rawOptions) {
434
376
  readDocumentValue(document, "username"),
435
377
  getPreviousValue(previous, "username")
436
378
  ])) {
437
- const user = await adapter.getUserByUsername(username);
379
+ const user = await realmHandle.user(username).get();
438
380
  if (user) return { user, duplicateEmailsAllowed };
439
381
  }
440
382
  }
@@ -443,9 +385,7 @@ function keycloakUserSyncPlugin(schema, rawOptions) {
443
385
  readDocumentValue(document, "email"),
444
386
  getPreviousValue(previous, "email")
445
387
  ])) {
446
- const matches = (await adapter.findUsersByEmail(email)).filter(
447
- (user) => normalizeEmail(user.email) === normalizeEmail(email)
448
- );
388
+ const matches = (await realmHandle.searchUsers(email, { attribute: "email", exact: true, first: 0, max: 2 })).filter((user) => normalizeEmail(user.email) === normalizeEmail(email));
449
389
  if (matches.length > 1) {
450
390
  throw new Error("Cannot identify a unique Keycloak user for the configured email identity");
451
391
  }
@@ -456,17 +396,15 @@ function keycloakUserSyncPlugin(schema, rawOptions) {
456
396
  return { user: null, duplicateEmailsAllowed };
457
397
  };
458
398
  const resolveCreatedUser = async (document, username) => {
459
- const userByUsername = await adapter.getUserByUsername(username);
399
+ const userByUsername = await realmHandle.user(username).get();
460
400
  if (userByUsername) return userByUsername;
461
401
  const email = stringValue(readDocumentValue(document, "email"));
462
402
  if (!email) return null;
463
- const matches = (await adapter.findUsersByEmail(email)).filter(
464
- (user) => normalizeEmail(user.email) === normalizeEmail(email)
465
- );
403
+ const matches = (await realmHandle.searchUsers(email, { attribute: "email", exact: true, first: 0, max: 2 })).filter((user) => normalizeEmail(user.email) === normalizeEmail(email));
466
404
  if (matches.length > 1) throw new Error("Cannot identify a unique Keycloak user for the configured email identity");
467
405
  const match = matches[0];
468
406
  if (!match?.id) return match ?? null;
469
- return await adapter.getUserById(match.username ?? match.id, match.id) ?? match;
407
+ return await realmHandle.userById(match.id).get() ?? match;
470
408
  };
471
409
  const syncRoles = async (user, document) => {
472
410
  if (!syncFields.roles || !user.id) return;
@@ -476,24 +414,12 @@ function keycloakUserSyncPlugin(schema, rawOptions) {
476
414
  if (desiredNames.length > maxRolesPerSync) {
477
415
  throw new Error(`keycloakUserSyncPlugin supports at most ${maxRolesPerSync} desired roles per sync operation`);
478
416
  }
479
- const managedNames = options.managedRoles ? new Set(options.managedRoles) : null;
480
- const effectiveDesiredNames = managedNames ? desiredNames.filter((name) => managedNames.has(name)) : desiredNames;
481
- if (effectiveDesiredNames.length === 0 && !managedNames) return;
482
- const desiredRoles = [];
483
- for (const roleName of effectiveDesiredNames) {
484
- if (options.ensureRoles !== false) await adapter.ensureRole(roleName);
485
- const role = await adapter.getRole(roleName);
486
- if (!role?.id) throw new Error(`Keycloak realm role "${roleName}" was not found`);
487
- desiredRoles.push(role);
488
- }
489
- const assignedRoles = await adapter.listRealmRoleMappings(user.id);
490
- const { toAdd, toRemove } = planRoleDiff(desiredNames, assignedRoles, desiredRoles, options.managedRoles);
491
- if (toAdd.length) {
492
- await adapter.addRealmRoleMappings(user.id, toAdd);
493
- }
494
- if (toRemove.length) {
495
- await adapter.delRealmRoleMappings(user.id, toRemove);
496
- }
417
+ if (desiredNames.length === 0 && !options.managedRoles) return;
418
+ await realmHandle.userById(user.id).reconcileRealmRoles(desiredNames, {
419
+ ensureMissing: options.ensureRoles !== false,
420
+ managedRoleNames: options.managedRoles,
421
+ maxRoles: maxRolesPerSync
422
+ });
497
423
  };
498
424
  const persistProviderId = async (document, providerId) => {
499
425
  if (options.persistProviderId === false || readDocumentValue(document, "providerId") === providerId) return;
@@ -520,14 +446,20 @@ function keycloakUserSyncPlugin(schema, rawOptions) {
520
446
  if (!username) throw new Error("Cannot create a Keycloak user without a username or email");
521
447
  payload.attributes = mergeAttributes(null, document, options);
522
448
  try {
523
- user = await adapter.createUser(username, payload) ?? null;
449
+ user = await realmHandle.user(username).create({
450
+ ...payload,
451
+ ...desiredPassword && {
452
+ password: desiredPassword,
453
+ passwordTemporary: options.passwordTemporary ?? false
454
+ }
455
+ }) ?? null;
456
+ created = true;
524
457
  } catch (error) {
525
458
  if (identities.length === 0) throw error;
526
459
  const recoveredUser = await resolveCreatedUser(document, username);
527
460
  if (!recoveredUser) throw error;
528
461
  user = recoveredUser;
529
462
  }
530
- created = true;
531
463
  }
532
464
  if (!user?.id) throw new Error("Keycloak did not return a user ID");
533
465
  const userId = user.id;
@@ -550,21 +482,21 @@ function keycloakUserSyncPlugin(schema, rawOptions) {
550
482
  }
551
483
  if (shouldSyncAttributes) payload.attributes = mergeAttributes(user, document, options);
552
484
  if (Object.keys(payload).length > 0) {
553
- await adapter.updateUser(userId, payload);
485
+ await realmHandle.userById(userId).update(payload);
554
486
  user = { ...user, ...payload };
555
487
  }
556
488
  }
557
- if ((created || state.passwordChanged) && desiredPassword) {
558
- await adapter.resetPassword(userId, desiredPassword, options.passwordTemporary ?? false);
489
+ if (!created && state.passwordChanged && desiredPassword) {
490
+ await realmHandle.userById(userId).resetPassword(desiredPassword, { temporary: options.passwordTemporary ?? false });
559
491
  }
560
492
  if (created || state.changedFields.has("roles")) await syncRoles(user, document);
561
493
  if (emailVerificationPlan.changed && options.sendVerificationEmailOnChange !== false) {
562
- await adapter.sendVerifyEmail(userId);
494
+ await realmHandle.userById(userId).sendVerifyEmail();
563
495
  }
564
496
  };
565
497
  const deleteDocument = async (document) => {
566
498
  const { user } = await resolveUser(document);
567
- if (user?.id) await adapter.deleteUser(user.id);
499
+ if (user?.id) await realmHandle.userById(user.id).delete();
568
500
  };
569
501
  schema.pre("save", async function keycloakUserSyncPreSave() {
570
502
  assertNoDocumentSession(this, "save");
@@ -613,5 +545,6 @@ function keycloakUserSyncPlugin(schema, rawOptions) {
613
545
  );
614
546
  }
615
547
  export {
548
+ createManagedKeycloakClient,
616
549
  keycloakUserSyncPlugin
617
550
  };