ldap-rest 0.0.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.
Files changed (296) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +48 -0
  3. package/bin/cleanup-external-users.mjs +219 -0
  4. package/bin/index.mjs +54 -0
  5. package/bin/sync-james.mjs +190 -0
  6. package/dist/abstract/ldapFlat.js +1 -0
  7. package/dist/abstract/plugin.js +1 -0
  8. package/dist/abstract/twakePlugin.js +1 -0
  9. package/dist/bin/index.js +1 -0
  10. package/dist/config/args.js +1 -0
  11. package/dist/lib/auth/base.js +1 -0
  12. package/dist/lib/authz/base.js +1 -0
  13. package/dist/lib/expressFormatedResponses.js +1 -0
  14. package/dist/lib/ldapActions.js +1 -0
  15. package/dist/lib/parseConfig.js +1 -0
  16. package/dist/lib/utils.js +1 -0
  17. package/dist/logger/winston.js +1 -0
  18. package/dist/plugins/auth/authzLinid1.js +1 -0
  19. package/dist/plugins/auth/authzPerBranch.js +1 -0
  20. package/dist/plugins/auth/crowdsec.js +1 -0
  21. package/dist/plugins/auth/llng.js +1 -0
  22. package/dist/plugins/auth/openidconnect.js +1 -0
  23. package/dist/plugins/auth/rateLimit.js +1 -0
  24. package/dist/plugins/auth/token.js +1 -0
  25. package/dist/plugins/configApi.js +1 -0
  26. package/dist/plugins/demo/helloworld.js +1 -0
  27. package/dist/plugins/ldap/bulkImport.js +1 -0
  28. package/dist/plugins/ldap/departmentSync.js +1 -0
  29. package/dist/plugins/ldap/externalUsersInGroups.js +1 -0
  30. package/dist/plugins/ldap/flatGeneric.js +1 -0
  31. package/dist/plugins/ldap/groups.js +1 -0
  32. package/dist/plugins/ldap/onChange.js +1 -0
  33. package/dist/plugins/ldap/organization.js +1 -0
  34. package/dist/plugins/ldap/trash.js +1 -0
  35. package/dist/plugins/priority.json.js +1 -0
  36. package/dist/plugins/static.js +1 -0
  37. package/dist/plugins/twake/calendarResources.js +1 -0
  38. package/dist/plugins/twake/james.js +1 -0
  39. package/dist/plugins/weblogs.js +1 -0
  40. package/dist/src/abstract/ldapFlat.d.ts +94 -0
  41. package/dist/src/abstract/plugin.d.ts +43 -0
  42. package/dist/src/abstract/twakePlugin.d.ts +90 -0
  43. package/dist/src/bin/index.d.ts +43 -0
  44. package/dist/src/browser/ldap-group-editor/LdapGroupEditor.d.ts +31 -0
  45. package/dist/src/browser/ldap-group-editor/api/GroupApiClient.d.ts +23 -0
  46. package/dist/src/browser/ldap-group-editor/components/CreateGroupModal.d.ts +19 -0
  47. package/dist/src/browser/ldap-group-editor/components/GroupPropertyEditor.d.ts +30 -0
  48. package/dist/src/browser/ldap-group-editor/components/GroupTree.d.ts +17 -0
  49. package/dist/src/browser/ldap-group-editor/components/MoveGroupModal.d.ts +20 -0
  50. package/dist/src/browser/ldap-group-editor/index.d.ts +6 -0
  51. package/dist/src/browser/ldap-group-editor/types.d.ts +58 -0
  52. package/dist/src/browser/ldap-resource-editor/api/ResourceApiClient.d.ts +53 -0
  53. package/dist/src/browser/ldap-resource-editor/cache/CacheManager.d.ts +65 -0
  54. package/dist/src/browser/ldap-resource-editor/index.d.ts +6 -0
  55. package/dist/src/browser/ldap-resource-editor/types.d.ts +78 -0
  56. package/dist/src/browser/ldap-tree-viewer/LdapTreeViewer.d.ts +26 -0
  57. package/dist/src/browser/ldap-tree-viewer/api/LdapApiClient.d.ts +30 -0
  58. package/dist/src/browser/ldap-tree-viewer/components/TreeNode.d.ts +17 -0
  59. package/dist/src/browser/ldap-tree-viewer/components/TreeRoot.d.ts +14 -0
  60. package/dist/src/browser/ldap-tree-viewer/index.d.ts +7 -0
  61. package/dist/src/browser/ldap-tree-viewer/store/Store.d.ts +18 -0
  62. package/dist/src/browser/ldap-tree-viewer/store/actions.d.ts +28 -0
  63. package/dist/src/browser/ldap-tree-viewer/store/reducers.d.ts +7 -0
  64. package/dist/src/browser/ldap-tree-viewer/types.d.ts +34 -0
  65. package/dist/src/browser/ldap-unit-editor/LdapUnitEditor.d.ts +27 -0
  66. package/dist/src/browser/ldap-unit-editor/api/UnitApiClient.d.ts +24 -0
  67. package/dist/src/browser/ldap-unit-editor/components/MoveUnitModal.d.ts +28 -0
  68. package/dist/src/browser/ldap-unit-editor/components/UnitPropertyEditor.d.ts +28 -0
  69. package/dist/src/browser/ldap-unit-editor/components/UnitTree.d.ts +18 -0
  70. package/dist/src/browser/ldap-unit-editor/index.d.ts +6 -0
  71. package/dist/src/browser/ldap-unit-editor/types.d.ts +55 -0
  72. package/dist/src/browser/ldap-user-editor/LdapUserEditor.d.ts +32 -0
  73. package/dist/src/browser/ldap-user-editor/api/UserApiClient.d.ts +55 -0
  74. package/dist/src/browser/ldap-user-editor/cache/CacheManager.d.ts +65 -0
  75. package/dist/src/browser/ldap-user-editor/components/MoveUserModal.d.ts +28 -0
  76. package/dist/src/browser/ldap-user-editor/components/PointerField.d.ts +22 -0
  77. package/dist/src/browser/ldap-user-editor/components/UserEditor.d.ts +46 -0
  78. package/dist/src/browser/ldap-user-editor/components/UserList.d.ts +24 -0
  79. package/dist/src/browser/ldap-user-editor/components/UserTree.d.ts +24 -0
  80. package/dist/src/browser/ldap-user-editor/index.d.ts +7 -0
  81. package/dist/src/browser/ldap-user-editor/types.d.ts +67 -0
  82. package/dist/src/browser/shared/components/Modal.d.ts +38 -0
  83. package/dist/src/browser/shared/components/StatusMessage.d.ts +48 -0
  84. package/dist/src/browser/shared/index.d.ts +10 -0
  85. package/dist/src/browser/shared/types/index.d.ts +65 -0
  86. package/dist/src/browser/shared/utils/dom.d.ts +33 -0
  87. package/dist/src/browser/shared/utils/form.d.ts +28 -0
  88. package/dist/src/browser/shared/utils/schema.d.ts +29 -0
  89. package/dist/src/config/args.d.ts +128 -0
  90. package/dist/src/config/schema.d.ts +18 -0
  91. package/dist/src/hooks.d.ts +76 -0
  92. package/dist/src/lib/auth/base.d.ts +9 -0
  93. package/dist/src/lib/authz/base.d.ts +63 -0
  94. package/dist/src/lib/expressFormatedResponses.d.ts +26 -0
  95. package/dist/src/lib/ldapActions.d.ts +91 -0
  96. package/dist/src/lib/parseConfig.d.ts +23 -0
  97. package/dist/src/lib/utils.d.ts +73 -0
  98. package/dist/src/logger/winston.d.ts +3 -0
  99. package/dist/src/plugins/auth/authzLinid1.d.ts +46 -0
  100. package/dist/src/plugins/auth/authzPerBranch.d.ts +59 -0
  101. package/dist/src/plugins/auth/crowdsec.d.ts +29 -0
  102. package/dist/src/plugins/auth/llng.d.ts +8 -0
  103. package/dist/src/plugins/auth/openidconnect.d.ts +11 -0
  104. package/dist/src/plugins/auth/rateLimit.d.ts +24 -0
  105. package/dist/src/plugins/auth/token.d.ts +17 -0
  106. package/dist/src/plugins/configApi.d.ts +24 -0
  107. package/dist/src/plugins/demo/helloworld.d.ts +12 -0
  108. package/dist/src/plugins/ldap/bulkImport.d.ts +48 -0
  109. package/dist/src/plugins/ldap/departmentSync.d.ts +35 -0
  110. package/dist/src/plugins/ldap/externalUsersInGroups.d.ts +21 -0
  111. package/dist/src/plugins/ldap/flatGeneric.d.ts +31 -0
  112. package/dist/src/plugins/ldap/groups.d.ts +95 -0
  113. package/dist/src/plugins/ldap/onChange.d.ts +32 -0
  114. package/dist/src/plugins/ldap/organization.d.ts +65 -0
  115. package/dist/src/plugins/ldap/trash.d.ts +52 -0
  116. package/dist/src/plugins/static.d.ts +7 -0
  117. package/dist/src/plugins/twake/calendarResources.d.ts +31 -0
  118. package/dist/src/plugins/twake/james.d.ts +83 -0
  119. package/dist/src/plugins/weblogs.d.ts +8 -0
  120. package/docs/DEVELOPER_GUIDE.md +298 -0
  121. package/docs/OPENAPI.md +440 -0
  122. package/docs/PLUGIN_DEPENDENCIES.md +612 -0
  123. package/docs/README.md +466 -0
  124. package/docs/api/REFERENCE.md +983 -0
  125. package/docs/api/REST_API.md +1237 -0
  126. package/docs/authentication.md +469 -0
  127. package/docs/authzLinid1.md +521 -0
  128. package/docs/authzPerBranch.md +596 -0
  129. package/docs/browser/LIBRARIES.md +1196 -0
  130. package/docs/examples/EXAMPLES.md +1659 -0
  131. package/docs/examples/twake-mailbox-type-nomenclature.ldif +37 -0
  132. package/docs/examples/twake-mailbox-type-schema-amendment.ldif +30 -0
  133. package/docs/jamesTeamMailboxes.md +343 -0
  134. package/docs/ldapBulkImport.md +722 -0
  135. package/docs/ldapExternalUsersInGroups.md +325 -0
  136. package/docs/ldapFlatGeneric.md +444 -0
  137. package/docs/ldapGroups.md +589 -0
  138. package/docs/ldapOrganizations.md +659 -0
  139. package/docs/ldapTrash.md +260 -0
  140. package/docs/linagora.png +0 -0
  141. package/docs/onChange.md +318 -0
  142. package/docs/plugins/DEVELOPMENT.md +1219 -0
  143. package/docs/presenterm-fr.md +721 -0
  144. package/docs/presenterm.md +721 -0
  145. package/docs/schemas/SCHEMAS.md +1212 -0
  146. package/docs/static.md +410 -0
  147. package/docs/twakeCalendarResources.md +148 -0
  148. package/docs/twakeJames.md +661 -0
  149. package/docs/weblogs.md +399 -0
  150. package/examples/web/ldap-group-editor.html +113 -0
  151. package/examples/web/ldap-tree-viewer.html +368 -0
  152. package/examples/web/ldap-unit-editor.html +108 -0
  153. package/examples/web/ldap-user-editor.html +412 -0
  154. package/package.json +458 -0
  155. package/static/browser/common-demo.css +450 -0
  156. package/static/browser/ldap-group-editor/LdapGroupEditor.js +207 -0
  157. package/static/browser/ldap-group-editor/LdapGroupEditor.js.map +1 -0
  158. package/static/browser/ldap-group-editor/api/GroupApiClient.js +112 -0
  159. package/static/browser/ldap-group-editor/api/GroupApiClient.js.map +1 -0
  160. package/static/browser/ldap-group-editor/components/CreateGroupModal.js +212 -0
  161. package/static/browser/ldap-group-editor/components/CreateGroupModal.js.map +1 -0
  162. package/static/browser/ldap-group-editor/components/GroupPropertyEditor.js +497 -0
  163. package/static/browser/ldap-group-editor/components/GroupPropertyEditor.js.map +1 -0
  164. package/static/browser/ldap-group-editor/components/GroupTree.js +153 -0
  165. package/static/browser/ldap-group-editor/components/GroupTree.js.map +1 -0
  166. package/static/browser/ldap-group-editor/components/MoveGroupModal.js +235 -0
  167. package/static/browser/ldap-group-editor/components/MoveGroupModal.js.map +1 -0
  168. package/static/browser/ldap-group-editor/index.js +2 -0
  169. package/static/browser/ldap-group-editor/index.js.map +1 -0
  170. package/static/browser/ldap-group-editor/types.js +2 -0
  171. package/static/browser/ldap-group-editor/types.js.map +1 -0
  172. package/static/browser/ldap-group-editor.esm.js +1399 -0
  173. package/static/browser/ldap-group-editor.esm.js.map +1 -0
  174. package/static/browser/ldap-group-editor.js +1407 -0
  175. package/static/browser/ldap-group-editor.js.map +1 -0
  176. package/static/browser/ldap-group-editor.min.js +2 -0
  177. package/static/browser/ldap-group-editor.min.js.map +1 -0
  178. package/static/browser/ldap-resource-editor/api/ResourceApiClient.js +226 -0
  179. package/static/browser/ldap-resource-editor/api/ResourceApiClient.js.map +1 -0
  180. package/static/browser/ldap-resource-editor/cache/CacheManager.js +123 -0
  181. package/static/browser/ldap-resource-editor/cache/CacheManager.js.map +1 -0
  182. package/static/browser/ldap-resource-editor/index.js +2 -0
  183. package/static/browser/ldap-resource-editor/index.js.map +1 -0
  184. package/static/browser/ldap-resource-editor/types.js +2 -0
  185. package/static/browser/ldap-resource-editor/types.js.map +1 -0
  186. package/static/browser/ldap-tree-viewer/LdapTreeViewer.js +187 -0
  187. package/static/browser/ldap-tree-viewer/LdapTreeViewer.js.map +1 -0
  188. package/static/browser/ldap-tree-viewer/api/LdapApiClient.js +50 -0
  189. package/static/browser/ldap-tree-viewer/api/LdapApiClient.js.map +1 -0
  190. package/static/browser/ldap-tree-viewer/components/TreeNode.js +140 -0
  191. package/static/browser/ldap-tree-viewer/components/TreeNode.js.map +1 -0
  192. package/static/browser/ldap-tree-viewer/components/TreeRoot.js +98 -0
  193. package/static/browser/ldap-tree-viewer/components/TreeRoot.js.map +1 -0
  194. package/static/browser/ldap-tree-viewer/index.js +2 -0
  195. package/static/browser/ldap-tree-viewer/index.js.map +1 -0
  196. package/static/browser/ldap-tree-viewer/store/Store.js +29 -0
  197. package/static/browser/ldap-tree-viewer/store/Store.js.map +1 -0
  198. package/static/browser/ldap-tree-viewer/store/actions.js +53 -0
  199. package/static/browser/ldap-tree-viewer/store/actions.js.map +1 -0
  200. package/static/browser/ldap-tree-viewer/store/reducers.js +79 -0
  201. package/static/browser/ldap-tree-viewer/store/reducers.js.map +1 -0
  202. package/static/browser/ldap-tree-viewer/types.js +2 -0
  203. package/static/browser/ldap-tree-viewer/types.js.map +1 -0
  204. package/static/browser/ldap-tree-viewer.css +2 -0
  205. package/static/browser/ldap-tree-viewer.css.map +1 -0
  206. package/static/browser/ldap-tree-viewer.esm.js +606 -0
  207. package/static/browser/ldap-tree-viewer.esm.js.map +1 -0
  208. package/static/browser/ldap-tree-viewer.js +614 -0
  209. package/static/browser/ldap-tree-viewer.js.map +1 -0
  210. package/static/browser/ldap-tree-viewer.min.js +2 -0
  211. package/static/browser/ldap-tree-viewer.min.js.map +1 -0
  212. package/static/browser/ldap-unit-editor/LdapUnitEditor.js +119 -0
  213. package/static/browser/ldap-unit-editor/LdapUnitEditor.js.map +1 -0
  214. package/static/browser/ldap-unit-editor/api/UnitApiClient.js +95 -0
  215. package/static/browser/ldap-unit-editor/api/UnitApiClient.js.map +1 -0
  216. package/static/browser/ldap-unit-editor/components/MoveUnitModal.js +295 -0
  217. package/static/browser/ldap-unit-editor/components/MoveUnitModal.js.map +1 -0
  218. package/static/browser/ldap-unit-editor/components/UnitPropertyEditor.js +321 -0
  219. package/static/browser/ldap-unit-editor/components/UnitPropertyEditor.js.map +1 -0
  220. package/static/browser/ldap-unit-editor/components/UnitTree.js +197 -0
  221. package/static/browser/ldap-unit-editor/components/UnitTree.js.map +1 -0
  222. package/static/browser/ldap-unit-editor/index.js +2 -0
  223. package/static/browser/ldap-unit-editor/index.js.map +1 -0
  224. package/static/browser/ldap-unit-editor/types.js +2 -0
  225. package/static/browser/ldap-unit-editor/types.js.map +1 -0
  226. package/static/browser/ldap-unit-editor.esm.js +1013 -0
  227. package/static/browser/ldap-unit-editor.esm.js.map +1 -0
  228. package/static/browser/ldap-unit-editor.js +1021 -0
  229. package/static/browser/ldap-unit-editor.js.map +1 -0
  230. package/static/browser/ldap-unit-editor.min.js +2 -0
  231. package/static/browser/ldap-unit-editor.min.js.map +1 -0
  232. package/static/browser/ldap-user-editor/LdapUserEditor.js +184 -0
  233. package/static/browser/ldap-user-editor/LdapUserEditor.js.map +1 -0
  234. package/static/browser/ldap-user-editor/api/UserApiClient.js +209 -0
  235. package/static/browser/ldap-user-editor/api/UserApiClient.js.map +1 -0
  236. package/static/browser/ldap-user-editor/cache/CacheManager.js +123 -0
  237. package/static/browser/ldap-user-editor/cache/CacheManager.js.map +1 -0
  238. package/static/browser/ldap-user-editor/components/MoveUserModal.js +302 -0
  239. package/static/browser/ldap-user-editor/components/MoveUserModal.js.map +1 -0
  240. package/static/browser/ldap-user-editor/components/PointerField.js +152 -0
  241. package/static/browser/ldap-user-editor/components/PointerField.js.map +1 -0
  242. package/static/browser/ldap-user-editor/components/UserEditor.js +608 -0
  243. package/static/browser/ldap-user-editor/components/UserEditor.js.map +1 -0
  244. package/static/browser/ldap-user-editor/components/UserList.js +181 -0
  245. package/static/browser/ldap-user-editor/components/UserList.js.map +1 -0
  246. package/static/browser/ldap-user-editor/components/UserTree.js +164 -0
  247. package/static/browser/ldap-user-editor/components/UserTree.js.map +1 -0
  248. package/static/browser/ldap-user-editor/index.js +2 -0
  249. package/static/browser/ldap-user-editor/index.js.map +1 -0
  250. package/static/browser/ldap-user-editor/types.js +2 -0
  251. package/static/browser/ldap-user-editor/types.js.map +1 -0
  252. package/static/browser/ldap-user-editor.css +2 -0
  253. package/static/browser/ldap-user-editor.css.map +1 -0
  254. package/static/browser/ldap-user-editor.esm.js +1897 -0
  255. package/static/browser/ldap-user-editor.esm.js.map +1 -0
  256. package/static/browser/ldap-user-editor.js +1905 -0
  257. package/static/browser/ldap-user-editor.js.map +1 -0
  258. package/static/browser/ldap-user-editor.min.js +2 -0
  259. package/static/browser/ldap-user-editor.min.js.map +1 -0
  260. package/static/browser/shared/components/Modal.js +121 -0
  261. package/static/browser/shared/components/Modal.js.map +1 -0
  262. package/static/browser/shared/components/StatusMessage.js +119 -0
  263. package/static/browser/shared/components/StatusMessage.js.map +1 -0
  264. package/static/browser/shared/index.js +7 -0
  265. package/static/browser/shared/index.js.map +1 -0
  266. package/static/browser/shared/types/index.js +37 -0
  267. package/static/browser/shared/types/index.js.map +1 -0
  268. package/static/browser/shared/utils/dom.js +78 -0
  269. package/static/browser/shared/utils/dom.js.map +1 -0
  270. package/static/browser/shared/utils/form.js +234 -0
  271. package/static/browser/shared/utils/form.js.map +1 -0
  272. package/static/browser/shared/utils/schema.js +121 -0
  273. package/static/browser/shared/utils/schema.js.map +1 -0
  274. package/static/browser/shared.esm.js +694 -0
  275. package/static/browser/shared.esm.js.map +1 -0
  276. package/static/browser/shared.js +723 -0
  277. package/static/browser/shared.js.map +1 -0
  278. package/static/browser/shared.min.js +2 -0
  279. package/static/browser/shared.min.js.map +1 -0
  280. package/static/schemas/ad/computers.json +92 -0
  281. package/static/schemas/ad/groups.json +55 -0
  282. package/static/schemas/ad/organizations.json +66 -0
  283. package/static/schemas/ad/users.json +116 -0
  284. package/static/schemas/standard/devices.json +72 -0
  285. package/static/schemas/standard/groups.json +54 -0
  286. package/static/schemas/standard/organizations.json +49 -0
  287. package/static/schemas/standard/users.json +86 -0
  288. package/static/schemas/twake/groups.json +75 -0
  289. package/static/schemas/twake/nomenclature/twakeAccountStatus.json +31 -0
  290. package/static/schemas/twake/nomenclature/twakeDeliveryMode.json +31 -0
  291. package/static/schemas/twake/nomenclature/twakeListType.json +31 -0
  292. package/static/schemas/twake/nomenclature/twakeMailboxType.json +31 -0
  293. package/static/schemas/twake/nomenclature/twakeTitle.json +31 -0
  294. package/static/schemas/twake/organizations.json +52 -0
  295. package/static/schemas/twake/positions.json +32 -0
  296. package/static/schemas/twake/users.json +112 -0
@@ -0,0 +1,1219 @@
1
+ # Plugin Development Guide
2
+
3
+ This guide will help you create powerful, production-ready plugins for LDAP-Rest.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Why Create a Plugin](#why-create-a-plugin)
8
+ - [Plugin Structure](#plugin-structure)
9
+ - [Creating Your Plugin](#creating-your-plugin)
10
+ - [Custom Configuration Options](#custom-configuration-options)
11
+ - [Loading Your Plugin](#loading-your-plugin)
12
+ - [Plugin Features](#plugin-features)
13
+ - [Configuration Best Practices](#configuration-best-practices)
14
+ - [Complete Plugin Example](#complete-plugin-example)
15
+ - [Testing Your Plugin](#testing-your-plugin)
16
+
17
+ ---
18
+
19
+ ## Why Create a Plugin
20
+
21
+ Plugins extend LDAP-Rest with custom functionality for your specific needs:
22
+
23
+ ### Common Use Cases
24
+
25
+ - **Business Logic** - Enforce organization-specific validation rules, data transformation, or custom workflows
26
+ - **External Integration** - Connect to external systems like email servers, webhooks, notification services, or third-party APIs
27
+ - **Authentication & Authorization** - Implement custom authentication methods, rate limiting, or fine-grained access control
28
+ - **API Endpoints** - Expose custom REST endpoints for specialized operations or reporting
29
+ - **Data Transformation** - Modify LDAP entries before/after operations, compute derived attributes, or maintain consistency
30
+
31
+ Plugins can combine multiple features: hook into LDAP operations, expose API endpoints, and integrate external services all in one module.
32
+
33
+ ---
34
+
35
+ ## Plugin Structure
36
+
37
+ Every plugin extends the `DmPlugin` base class and defines a unique name:
38
+
39
+ ```typescript
40
+ import DmPlugin from '../../abstract/plugin';
41
+ import type { Role } from '../../abstract/plugin';
42
+
43
+ export default class MyPlugin extends DmPlugin {
44
+ name = 'myPlugin'; // Unique identifier for your plugin
45
+ roles: Role[] = ['api'] as const; // Optional: categorize your plugin
46
+ }
47
+ ```
48
+
49
+ ### Core Properties
50
+
51
+ - **name** (required): Unique identifier for your plugin
52
+ - **roles** (optional): Array categorizing plugin purpose
53
+ - **dependencies** (optional): Required plugins that must load first
54
+ - **hooks** (optional): Callbacks for LDAP operations
55
+ - **api()** (optional): Method to register REST endpoints
56
+
57
+ ### Available from Base Class
58
+
59
+ - **this.server**: DM server instance
60
+ - **this.config**: Complete configuration object
61
+ - **this.logger**: Winston logger instance
62
+ - **this.registeredHooks**: Access to global hook registry
63
+
64
+ ---
65
+
66
+ ## Creating Your Plugin
67
+
68
+ ### Option 1: Core Plugin (Inside ldap-rest Repository)
69
+
70
+ Create a file in `src/plugins/<category>/<pluginName>.ts`:
71
+
72
+ ```typescript
73
+ /**
74
+ * @module plugins/notification/webhook
75
+ * @author Your Name <your.email@example.com>
76
+ *
77
+ * Webhook notification plugin that sends HTTP requests on LDAP changes
78
+ */
79
+ import type { Express, Request, Response } from 'express';
80
+
81
+ import DmPlugin, { type Role } from '../../abstract/plugin';
82
+ import type { DM } from '../../bin';
83
+ import type { Hooks } from '../../hooks';
84
+
85
+ export default class WebhookNotifier extends DmPlugin {
86
+ name = 'webhookNotifier';
87
+ roles: Role[] = ['api'] as const;
88
+
89
+ // Declare dependencies (optional)
90
+ dependencies = {
91
+ ldapGroups: 'core/ldap/groups',
92
+ };
93
+
94
+ constructor(server: DM) {
95
+ super(server);
96
+
97
+ // Validate configuration
98
+ if (!this.config.webhook_url) {
99
+ throw new Error('--webhook-url is required');
100
+ }
101
+
102
+ this.logger.info('WebhookNotifier initialized');
103
+ }
104
+
105
+ // Register API endpoints
106
+ api(app: Express): void {
107
+ app.get(`${this.config.api_prefix}/v1/webhook/status`, (req, res) => {
108
+ res.json({
109
+ status: 'active',
110
+ url: this.config.webhook_url,
111
+ });
112
+ });
113
+ }
114
+
115
+ // Register hooks
116
+ hooks: Hooks = {
117
+ ldapadddone: async ([dn, entry]) => {
118
+ this.logger.info(`User added: ${dn}`);
119
+ await this.sendWebhook('user.added', { dn, entry });
120
+ },
121
+ };
122
+
123
+ // Private methods
124
+ private async sendWebhook(event: string, data: unknown): Promise<void> {
125
+ try {
126
+ await fetch(this.config.webhook_url as string, {
127
+ method: 'POST',
128
+ headers: { 'Content-Type': 'application/json' },
129
+ body: JSON.stringify({ event, data }),
130
+ });
131
+ } catch (error) {
132
+ this.logger.error('Webhook failed:', error);
133
+ }
134
+ }
135
+ }
136
+ ```
137
+
138
+ Load with:
139
+
140
+ ```bash
141
+ npx ldap-rest \
142
+ --plugin core/notification/webhook \
143
+ --webhook-url https://example.com/webhook
144
+ ```
145
+
146
+ ### Option 2: External Plugin (Outside Repository)
147
+
148
+ Create a standalone file anywhere on your system, e.g., `/opt/custom-plugins/my-plugin.ts`:
149
+
150
+ ```typescript
151
+ /**
152
+ * External plugin example
153
+ * This plugin can be loaded from anywhere
154
+ */
155
+ import type { Express } from 'express';
156
+
157
+ // Import from ldap-rest (if installed as npm package)
158
+ // import DmPlugin from 'ldap-rest/plugin';
159
+
160
+ // Or use relative path to node_modules
161
+ import DmPlugin from './node_modules/ldap-rest/dist/abstract/plugin.js';
162
+ import type { DM } from './node_modules/ldap-rest/dist/bin/index.js';
163
+ import type { Hooks } from './node_modules/ldap-rest/dist/hooks.js';
164
+
165
+ export default class ExternalAuditPlugin extends DmPlugin {
166
+ name = 'externalAudit';
167
+
168
+ constructor(server: DM) {
169
+ super(server);
170
+
171
+ // Access custom configuration options
172
+ const auditPath = this.config.audit_path || '/var/log/ldap-audit.log';
173
+ this.logger.info(`Audit log path: ${auditPath}`);
174
+ }
175
+
176
+ api(app: Express): void {
177
+ app.get(`${this.config.api_prefix}/v1/audit/stats`, (req, res) => {
178
+ res.json({
179
+ message: 'External plugin API',
180
+ auditEnabled: true,
181
+ });
182
+ });
183
+ }
184
+
185
+ hooks: Hooks = {
186
+ ldapaddrequest: async ([dn, entry, req]) => {
187
+ this.logger.debug(`[AUDIT] Adding: ${dn}`);
188
+ // Your audit logic here
189
+ return [dn, entry, req];
190
+ },
191
+
192
+ ldapmodifyrequest: async ([dn, changes, op]) => {
193
+ this.logger.debug(`[AUDIT] Modifying: ${dn}`);
194
+ // Your audit logic here
195
+ return [dn, changes, op];
196
+ },
197
+ };
198
+ }
199
+ ```
200
+
201
+ Load with:
202
+
203
+ ```bash
204
+ npx ldap-rest \
205
+ --plugin /opt/custom-plugins/my-plugin.ts \
206
+ --audit-path /var/log/ldap-audit.log
207
+ ```
208
+
209
+ Or via environment variable:
210
+
211
+ ```bash
212
+ export DM_PLUGINS=/opt/custom-plugins/my-plugin.ts
213
+ export DM_AUDIT_PATH=/var/log/ldap-audit.log
214
+ npx ldap-rest
215
+ ```
216
+
217
+ ---
218
+
219
+ ## Custom Configuration Options
220
+
221
+ LDAP-Rest automatically accepts and stores any unknown command-line options in the config object. This allows your plugin to define custom configuration without modifying core files.
222
+
223
+ ### How It Works
224
+
225
+ 1. **Command-line option format**: `--option-name value`
226
+ 2. **Converted to config key**: `option_name` (dashes become underscores)
227
+ 3. **Environment variable format**: `DM_OPTION_NAME` (uppercase with underscores)
228
+ 4. **Access in plugin**: `this.config.option_name`
229
+
230
+ ### Single Value Options
231
+
232
+ ```bash
233
+ # Command line
234
+ npx ldap-rest \
235
+ --webhook-url https://example.com/webhook \
236
+ --webhook-timeout 5000 \
237
+ --webhook-retry true
238
+
239
+ # Environment variables
240
+ export DM_WEBHOOK_URL=https://example.com/webhook
241
+ export DM_WEBHOOK_TIMEOUT=5000
242
+ export DM_WEBHOOK_RETRY=true
243
+ ```
244
+
245
+ In your plugin:
246
+
247
+ ```typescript
248
+ constructor(server: DM) {
249
+ super(server);
250
+
251
+ // Access custom options (converted to snake_case)
252
+ const url = this.config.webhook_url; // string
253
+ const timeout = this.config.webhook_timeout; // string (convert if needed)
254
+ const retry = this.config.webhook_retry; // string 'true' (convert if needed)
255
+
256
+ // Type conversion
257
+ const timeoutNum = parseInt(timeout as string || '3000');
258
+ const retryBool = (retry as string) === 'true';
259
+ }
260
+ ```
261
+
262
+ ### Multiple Value Options
263
+
264
+ Repeat the same option multiple times to create an array:
265
+
266
+ ```bash
267
+ # Command line
268
+ npx ldap-rest \
269
+ --allowed-domain example.com \
270
+ --allowed-domain test.com \
271
+ --allowed-domain dev.com
272
+
273
+ # Environment variable (comma or semicolon separated)
274
+ export DM_ALLOWED_DOMAINS=example.com,test.com,dev.com
275
+ ```
276
+
277
+ In your plugin:
278
+
279
+ ```typescript
280
+ constructor(server: DM) {
281
+ super(server);
282
+
283
+ // Access as array (note: singular becomes plural in config key)
284
+ const domains = this.config.allowed_domain as string[];
285
+ // or sometimes stored as: this.config.allowed_domains
286
+
287
+ this.logger.info(`Allowed domains: ${domains.join(', ')}`);
288
+ }
289
+ ```
290
+
291
+ ### Configuration Priority
292
+
293
+ Values are resolved in this order (later overrides earlier):
294
+
295
+ 1. **Default value** in your plugin
296
+ 2. **Environment variable** (`DM_OPTION_NAME`)
297
+ 3. **Command-line argument** (`--option-name`)
298
+
299
+ ```typescript
300
+ constructor(server: DM) {
301
+ super(server);
302
+
303
+ // Provide defaults for optional configuration
304
+ const webhookUrl = this.config.webhook_url || 'http://localhost:8080/webhook';
305
+ const webhookTimeout = parseInt(this.config.webhook_timeout as string || '3000');
306
+ const maxRetries = parseInt(this.config.max_retries as string || '3');
307
+
308
+ // Validate required configuration
309
+ if (!this.config.webhook_secret) {
310
+ throw new Error('--webhook-secret is required for authentication');
311
+ }
312
+ }
313
+ ```
314
+
315
+ ---
316
+
317
+ ## Loading Your Plugin
318
+
319
+ ### Core Plugin
320
+
321
+ ```bash
322
+ # Load by path within src/plugins/
323
+ npx ldap-rest --plugin core/notification/webhook
324
+
325
+ # Load multiple plugins
326
+ npx ldap-rest \
327
+ --plugin core/ldap/groups \
328
+ --plugin core/notification/webhook \
329
+ --plugin core/static
330
+ ```
331
+
332
+ ### External Plugin
333
+
334
+ ```bash
335
+ # Load by absolute file path
336
+ npx ldap-rest --plugin /path/to/my-plugin.ts
337
+
338
+ # Load multiple external plugins
339
+ npx ldap-rest \
340
+ --plugin /opt/plugins/audit.ts \
341
+ --plugin /opt/plugins/validation.ts
342
+ ```
343
+
344
+ ### Via Environment Variable
345
+
346
+ ```bash
347
+ # Single plugin
348
+ export DM_PLUGINS=core/notification/webhook
349
+ npx ldap-rest
350
+
351
+ # Multiple plugins (comma-separated)
352
+ export DM_PLUGINS=core/ldap/groups,core/static,/opt/plugins/custom.ts
353
+ npx ldap-rest
354
+ ```
355
+
356
+ ### Plugin Loading Order
357
+
358
+ Some plugins must load before others (e.g., authentication plugins). Edit `src/plugins/priority.json`:
359
+
360
+ ```json
361
+ [
362
+ "core/auth/token",
363
+ "core/auth/llng",
364
+ "core/auth/openidconnect",
365
+ "core/auth/authzPerBranch",
366
+ "core/myNewAuthPlugin"
367
+ ]
368
+ ```
369
+
370
+ Plugins in this list load first, in order. All other plugins load after.
371
+
372
+ ---
373
+
374
+ ## Plugin Features
375
+
376
+ ### Declaring Dependencies
377
+
378
+ Ensure required plugins load before yours:
379
+
380
+ ```typescript
381
+ export default class MyPlugin extends DmPlugin {
382
+ name = 'myPlugin';
383
+
384
+ dependencies = {
385
+ // Key is how you reference it, value is the plugin path
386
+ groups: 'core/ldap/groups',
387
+ users: 'core/ldap/flatGeneric',
388
+ };
389
+
390
+ constructor(server: DM) {
391
+ super(server);
392
+
393
+ // Access dependent plugin
394
+ const groupsPlugin = this.server.loadedPlugins['ldapGroups'];
395
+ if (groupsPlugin) {
396
+ this.logger.info('Groups plugin is available');
397
+ }
398
+ }
399
+ }
400
+ ```
401
+
402
+ ### Using LDAP Operations
403
+
404
+ The `this.server.ldap` object provides access to all LDAP operations:
405
+
406
+ #### Search
407
+
408
+ ```typescript
409
+ // Basic search
410
+ const results = await this.server.ldap.search(
411
+ {
412
+ paged: false,
413
+ scope: 'sub', // 'base', 'one', or 'sub'
414
+ filter: '(objectClass=inetOrgPerson)',
415
+ attributes: ['cn', 'mail', 'uid'],
416
+ },
417
+ 'ou=users,dc=example,dc=com'
418
+ );
419
+
420
+ // Search with pagination (returns AsyncGenerator)
421
+ const pagedResults = await this.server.ldap.search(
422
+ {
423
+ paged: true,
424
+ filter: '(uid=*)',
425
+ attributes: ['cn', 'mail'],
426
+ },
427
+ 'dc=example,dc=com'
428
+ );
429
+
430
+ for await (const page of pagedResults) {
431
+ page.searchEntries.forEach(entry => {
432
+ console.log(entry.dn, entry.cn);
433
+ });
434
+ }
435
+ ```
436
+
437
+ #### Add
438
+
439
+ ```typescript
440
+ await this.server.ldap.add('uid=john,ou=users,dc=example,dc=com', {
441
+ objectClass: ['inetOrgPerson', 'person', 'top'],
442
+ uid: 'john',
443
+ cn: 'John Doe',
444
+ sn: 'Doe',
445
+ mail: 'john@example.com',
446
+ userPassword: 'secret123',
447
+ });
448
+ ```
449
+
450
+ #### Modify
451
+
452
+ ```typescript
453
+ await this.server.ldap.modify('uid=john,ou=users,dc=example,dc=com', {
454
+ replace: {
455
+ mail: 'john.doe@example.com',
456
+ telephoneNumber: '+1234567890',
457
+ },
458
+ add: {
459
+ description: 'Senior Developer',
460
+ },
461
+ delete: {
462
+ oldAttribute: 'valueToRemove',
463
+ },
464
+ });
465
+ ```
466
+
467
+ #### Delete
468
+
469
+ ```typescript
470
+ // Delete single entry
471
+ await this.server.ldap.delete('uid=john,ou=users,dc=example,dc=com');
472
+
473
+ // Delete multiple entries
474
+ await this.server.ldap.delete([
475
+ 'uid=user1,ou=users,dc=example,dc=com',
476
+ 'uid=user2,ou=users,dc=example,dc=com',
477
+ ]);
478
+ ```
479
+
480
+ ### Registering Hooks
481
+
482
+ Hooks let you intercept and modify LDAP operations:
483
+
484
+ ```typescript
485
+ hooks: Hooks = {
486
+ // LDAP ADD HOOKS
487
+ ldapaddrequest: async ([dn, entry, req]) => {
488
+ // Called BEFORE adding an entry
489
+ // Validate, modify entry, or prevent operation
490
+ this.logger.debug(`Adding entry: ${dn}`);
491
+
492
+ // Add computed field
493
+ entry.createdAt = new Date().toISOString();
494
+
495
+ // Validation
496
+ if (!entry.mail) {
497
+ throw new Error('Email is required');
498
+ }
499
+
500
+ return [dn, entry, req];
501
+ },
502
+
503
+ ldapadddone: async ([dn, entry]) => {
504
+ // Called AFTER successful add
505
+ this.logger.info(`Entry added: ${dn}`);
506
+ // Trigger side effects, notifications, etc.
507
+ },
508
+
509
+ // LDAP MODIFY HOOKS
510
+ ldapmodifyrequest: async ([dn, changes, opNumber]) => {
511
+ // Called BEFORE modifying an entry
512
+ this.logger.debug(`Modifying entry: ${dn}`);
513
+
514
+ // Add audit trail
515
+ if (!changes.add) changes.add = {};
516
+ changes.add.modifiedAt = new Date().toISOString();
517
+
518
+ return [dn, changes, opNumber];
519
+ },
520
+
521
+ ldapmodifydone: async ([dn, changes, opNumber]) => {
522
+ // Called AFTER successful modify
523
+ this.logger.info(`Entry modified: ${dn}`);
524
+ },
525
+
526
+ // LDAP DELETE HOOKS
527
+ ldapdeleterequest: async dn => {
528
+ // Called BEFORE deleting entry
529
+ this.logger.debug(`Deleting entry: ${dn}`);
530
+
531
+ // Prevent deletion of admin
532
+ if (dn.includes('cn=admin')) {
533
+ throw new Error('Cannot delete admin user');
534
+ }
535
+
536
+ return dn;
537
+ },
538
+
539
+ ldapdeletedone: async dn => {
540
+ // Called AFTER successful delete
541
+ this.logger.info(`Entry deleted: ${dn}`);
542
+ },
543
+
544
+ // CUSTOM HOOKS (from other plugins)
545
+ onLdapChange: async (dn, changes) => {
546
+ // React to any LDAP change
547
+ this.logger.info(`Change detected on ${dn}`);
548
+ },
549
+ };
550
+ ```
551
+
552
+ ### Exposing API Endpoints
553
+
554
+ Create REST API endpoints for your plugin:
555
+
556
+ ```typescript
557
+ api(app: Express): void {
558
+ // GET endpoint
559
+ app.get(
560
+ `${this.config.api_prefix}/v1/myplugin/status`,
561
+ async (req: Request, res: Response) => {
562
+ try {
563
+ const status = await this.getStatus();
564
+ res.json({ success: true, data: status });
565
+ } catch (error) {
566
+ res.status(500).json({
567
+ success: false,
568
+ error: error.message
569
+ });
570
+ }
571
+ }
572
+ );
573
+
574
+ // POST endpoint with body
575
+ app.post(
576
+ `${this.config.api_prefix}/v1/myplugin/process`,
577
+ async (req: Request, res: Response) => {
578
+ try {
579
+ // Validate request body
580
+ if (!req.body || !req.body.data) {
581
+ return res.status(400).json({
582
+ success: false,
583
+ error: 'data is required'
584
+ });
585
+ }
586
+
587
+ const result = await this.processData(req.body.data);
588
+ res.json({ success: true, data: result });
589
+ } catch (error) {
590
+ this.logger.error('Process failed:', error);
591
+ res.status(500).json({
592
+ success: false,
593
+ error: error.message
594
+ });
595
+ }
596
+ }
597
+ );
598
+
599
+ // PUT endpoint
600
+ app.put(
601
+ `${this.config.api_prefix}/v1/myplugin/config`,
602
+ async (req: Request, res: Response) => {
603
+ try {
604
+ await this.updateConfig(req.body);
605
+ res.json({ success: true });
606
+ } catch (error) {
607
+ res.status(500).json({ success: false, error: error.message });
608
+ }
609
+ }
610
+ );
611
+
612
+ // DELETE endpoint
613
+ app.delete(
614
+ `${this.config.api_prefix}/v1/myplugin/cache`,
615
+ async (req: Request, res: Response) => {
616
+ try {
617
+ await this.clearCache();
618
+ res.json({ success: true, message: 'Cache cleared' });
619
+ } catch (error) {
620
+ res.status(500).json({ success: false, error: error.message });
621
+ }
622
+ }
623
+ );
624
+
625
+ this.logger.info('MyPlugin API endpoints registered');
626
+ }
627
+ ```
628
+
629
+ ### Logging
630
+
631
+ Use the provided logger with appropriate levels:
632
+
633
+ ```typescript
634
+ // Debug - detailed diagnostic information
635
+ this.logger.debug('Processing entry:', entry);
636
+
637
+ // Info - general informational messages
638
+ this.logger.info('Plugin initialized successfully');
639
+
640
+ // Warn - warning messages for potentially harmful situations
641
+ this.logger.warn('Configuration not found, using defaults');
642
+
643
+ // Error - error events that might still allow the application to continue
644
+ this.logger.error('Failed to process webhook:', error);
645
+ ```
646
+
647
+ ### Plugin Roles
648
+
649
+ Categorize your plugin using roles:
650
+
651
+ ```typescript
652
+ export default class MyPlugin extends DmPlugin {
653
+ name = 'myPlugin';
654
+ roles: Role[] = ['auth', 'api'] as const;
655
+ }
656
+ ```
657
+
658
+ Available roles:
659
+
660
+ - **auth** - Authentication plugins
661
+ - **authz** - Authorization/access control plugins
662
+ - **protect** - Security/rate limiting plugins
663
+ - **api** - Plugins exposing API endpoints
664
+ - **logging** - Logging and audit plugins
665
+ - **demo** - Example/demonstration plugins
666
+ - **consistency** - Data consistency enforcement plugins
667
+
668
+ ---
669
+
670
+ ## Configuration Best Practices
671
+
672
+ ### Validation in Constructor
673
+
674
+ ```typescript
675
+ constructor(server: DM) {
676
+ super(server);
677
+
678
+ // Validate required configuration
679
+ if (!this.config.webhook_url) {
680
+ throw new Error('Missing required configuration: --webhook-url');
681
+ }
682
+
683
+ // Validate format
684
+ const url = this.config.webhook_url as string;
685
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
686
+ throw new Error('--webhook-url must start with http:// or https://');
687
+ }
688
+
689
+ // Validate numeric ranges
690
+ const timeout = parseInt(this.config.webhook_timeout as string || '3000');
691
+ if (timeout < 100 || timeout > 30000) {
692
+ throw new Error('--webhook-timeout must be between 100 and 30000');
693
+ }
694
+
695
+ this.logger.info('Configuration validated successfully');
696
+ }
697
+ ```
698
+
699
+ ### Provide Defaults
700
+
701
+ ```typescript
702
+ constructor(server: DM) {
703
+ super(server);
704
+
705
+ // Set defaults for optional configuration
706
+ this.webhookUrl = this.config.webhook_url as string;
707
+ this.webhookTimeout = parseInt(
708
+ this.config.webhook_timeout as string || '3000'
709
+ );
710
+ this.maxRetries = parseInt(
711
+ this.config.webhook_max_retries as string || '3'
712
+ );
713
+ this.retryDelay = parseInt(
714
+ this.config.webhook_retry_delay as string || '1000'
715
+ );
716
+ }
717
+ ```
718
+
719
+ ### Document Configuration Options
720
+
721
+ Add clear documentation in your plugin file:
722
+
723
+ ```typescript
724
+ /**
725
+ * @module plugins/notification/webhook
726
+ * @author Your Name <your.email@example.com>
727
+ *
728
+ * Webhook notification plugin
729
+ *
730
+ * Configuration options:
731
+ *
732
+ * Required:
733
+ * - --webhook-url <url> Webhook endpoint URL (required)
734
+ * - --webhook-secret <string> Secret for webhook authentication (required)
735
+ *
736
+ * Optional:
737
+ * - --webhook-timeout <ms> Request timeout in milliseconds (default: 3000)
738
+ * - --webhook-max-retries <n> Maximum retry attempts (default: 3)
739
+ * - --webhook-retry-delay <ms> Delay between retries (default: 1000)
740
+ * - --webhook-event <event> Events to send (multiple, default: all)
741
+ *
742
+ * Environment variables:
743
+ * - DM_WEBHOOK_URL
744
+ * - DM_WEBHOOK_SECRET
745
+ * - DM_WEBHOOK_TIMEOUT
746
+ * - DM_WEBHOOK_MAX_RETRIES
747
+ * - DM_WEBHOOK_RETRY_DELAY
748
+ * - DM_WEBHOOK_EVENTS (comma-separated)
749
+ *
750
+ * Example usage:
751
+ *
752
+ * npx ldap-rest \
753
+ * --plugin core/notification/webhook \
754
+ * --webhook-url https://example.com/webhook \
755
+ * --webhook-secret mysecret123 \
756
+ * --webhook-event user.added \
757
+ * --webhook-event user.modified
758
+ */
759
+ ```
760
+
761
+ ---
762
+
763
+ ## Complete Plugin Example
764
+
765
+ Here's a complete, production-ready plugin that sends webhook notifications on LDAP changes:
766
+
767
+ ```typescript
768
+ /**
769
+ * @module plugins/notification/webhook
770
+ * @author Example Developer <dev@example.com>
771
+ *
772
+ * Sends webhook notifications when LDAP entries are modified
773
+ *
774
+ * Configuration:
775
+ * - --webhook-url <url> Target webhook URL (required)
776
+ * - --webhook-secret <string> Authentication secret (required)
777
+ * - --webhook-timeout <ms> Request timeout (default: 5000)
778
+ * - --webhook-retry-count <n> Retry attempts (default: 3)
779
+ * - --webhook-event <event> Events to monitor (multiple allowed)
780
+ */
781
+ import type { Express, Request, Response } from 'express';
782
+ import crypto from 'crypto';
783
+
784
+ import DmPlugin, { type Role } from '../../abstract/plugin';
785
+ import type { DM } from '../../bin';
786
+ import type { Hooks } from '../../hooks';
787
+ import type { AttributesList, ModifyRequest } from '../../lib/ldapActions';
788
+
789
+ interface WebhookPayload {
790
+ event: string;
791
+ timestamp: string;
792
+ dn: string;
793
+ data: AttributesList | ModifyRequest;
794
+ }
795
+
796
+ interface WebhookStats {
797
+ sent: number;
798
+ failed: number;
799
+ lastSent: string | null;
800
+ lastError: string | null;
801
+ }
802
+
803
+ export default class NotificationPlugin extends DmPlugin {
804
+ name = 'notification';
805
+ roles: Role[] = ['api', 'logging'] as const;
806
+
807
+ private webhookUrl: string;
808
+ private webhookSecret: string;
809
+ private webhookTimeout: number;
810
+ private retryCount: number;
811
+ private enabledEvents: Set<string>;
812
+ private stats: WebhookStats;
813
+
814
+ constructor(server: DM) {
815
+ super(server);
816
+
817
+ // Validate required configuration
818
+ if (!this.config.webhook_url) {
819
+ throw new Error('NotificationPlugin requires --webhook-url');
820
+ }
821
+ if (!this.config.webhook_secret) {
822
+ throw new Error('NotificationPlugin requires --webhook-secret');
823
+ }
824
+
825
+ // Load configuration with defaults
826
+ this.webhookUrl = this.config.webhook_url as string;
827
+ this.webhookSecret = this.config.webhook_secret as string;
828
+ this.webhookTimeout = parseInt(
829
+ (this.config.webhook_timeout as string) || '5000'
830
+ );
831
+ this.retryCount = parseInt(
832
+ (this.config.webhook_retry_count as string) || '3'
833
+ );
834
+
835
+ // Parse enabled events
836
+ const events = (this.config.webhook_event as string[]) || ['*'];
837
+ this.enabledEvents = new Set(events);
838
+
839
+ // Initialize stats
840
+ this.stats = {
841
+ sent: 0,
842
+ failed: 0,
843
+ lastSent: null,
844
+ lastError: null,
845
+ };
846
+
847
+ this.logger.info(
848
+ `NotificationPlugin initialized: ${this.webhookUrl} ` +
849
+ `(events: ${Array.from(this.enabledEvents).join(', ')})`
850
+ );
851
+ }
852
+
853
+ api(app: Express): void {
854
+ // Get webhook stats
855
+ app.get(
856
+ `${this.config.api_prefix}/v1/notifications/stats`,
857
+ (req: Request, res: Response) => {
858
+ res.json({
859
+ success: true,
860
+ data: {
861
+ ...this.stats,
862
+ webhookUrl: this.webhookUrl,
863
+ enabledEvents: Array.from(this.enabledEvents),
864
+ },
865
+ });
866
+ }
867
+ );
868
+
869
+ // Test webhook
870
+ app.post(
871
+ `${this.config.api_prefix}/v1/notifications/test`,
872
+ async (req: Request, res: Response) => {
873
+ try {
874
+ await this.sendWebhook('test', 'test', { message: 'Test webhook' });
875
+ res.json({ success: true, message: 'Test webhook sent' });
876
+ } catch (error) {
877
+ res.status(500).json({
878
+ success: false,
879
+ error: (error as Error).message,
880
+ });
881
+ }
882
+ }
883
+ );
884
+
885
+ this.logger.info('NotificationPlugin API registered');
886
+ }
887
+
888
+ hooks: Hooks = {
889
+ ldapadddone: async ([dn, entry]) => {
890
+ if (this.shouldSendEvent('ldap.add')) {
891
+ void this.sendWebhook('ldap.add', dn, entry);
892
+ }
893
+ },
894
+
895
+ ldapmodifydone: async ([dn, changes, _op]) => {
896
+ if (this.shouldSendEvent('ldap.modify')) {
897
+ void this.sendWebhook('ldap.modify', dn, changes);
898
+ }
899
+ },
900
+
901
+ ldapdeletedone: async dn => {
902
+ if (this.shouldSendEvent('ldap.delete')) {
903
+ const dnStr = Array.isArray(dn) ? dn.join(', ') : dn;
904
+ void this.sendWebhook('ldap.delete', dnStr, {});
905
+ }
906
+ },
907
+ };
908
+
909
+ /**
910
+ * Check if an event should trigger webhook
911
+ */
912
+ private shouldSendEvent(event: string): boolean {
913
+ return this.enabledEvents.has('*') || this.enabledEvents.has(event);
914
+ }
915
+
916
+ /**
917
+ * Send webhook notification with retry logic
918
+ */
919
+ private async sendWebhook(
920
+ event: string,
921
+ dn: string,
922
+ data: AttributesList | ModifyRequest
923
+ ): Promise<void> {
924
+ const payload: WebhookPayload = {
925
+ event,
926
+ timestamp: new Date().toISOString(),
927
+ dn,
928
+ data,
929
+ };
930
+
931
+ let lastError: Error | null = null;
932
+
933
+ for (let attempt = 0; attempt <= this.retryCount; attempt++) {
934
+ try {
935
+ await this.sendWebhookRequest(payload);
936
+
937
+ // Success
938
+ this.stats.sent++;
939
+ this.stats.lastSent = payload.timestamp;
940
+ this.logger.info(`Webhook sent: ${event} ${dn}`);
941
+ return;
942
+ } catch (error) {
943
+ lastError = error as Error;
944
+
945
+ if (attempt < this.retryCount) {
946
+ this.logger.warn(
947
+ `Webhook attempt ${attempt + 1} failed, retrying...`
948
+ );
949
+ await this.delay(1000 * (attempt + 1));
950
+ }
951
+ }
952
+ }
953
+
954
+ // All attempts failed
955
+ this.stats.failed++;
956
+ this.stats.lastError = lastError?.message || 'Unknown error';
957
+ this.logger.error(
958
+ `Webhook failed after ${this.retryCount + 1} attempts:`,
959
+ lastError
960
+ );
961
+ }
962
+
963
+ /**
964
+ * Send single webhook HTTP request
965
+ */
966
+ private async sendWebhookRequest(payload: WebhookPayload): Promise<void> {
967
+ const body = JSON.stringify(payload);
968
+ const signature = this.generateSignature(body);
969
+
970
+ const controller = new AbortController();
971
+ const timeoutId = setTimeout(() => controller.abort(), this.webhookTimeout);
972
+
973
+ try {
974
+ const response = await fetch(this.webhookUrl, {
975
+ method: 'POST',
976
+ headers: {
977
+ 'Content-Type': 'application/json',
978
+ 'X-Webhook-Signature': signature,
979
+ 'User-Agent': 'LDAP-Rest-Webhook/1.0',
980
+ },
981
+ body,
982
+ signal: controller.signal,
983
+ });
984
+
985
+ if (!response.ok) {
986
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
987
+ }
988
+ } finally {
989
+ clearTimeout(timeoutId);
990
+ }
991
+ }
992
+
993
+ /**
994
+ * Generate HMAC signature for webhook authentication
995
+ */
996
+ private generateSignature(payload: string): string {
997
+ return crypto
998
+ .createHmac('sha256', this.webhookSecret)
999
+ .update(payload)
1000
+ .digest('hex');
1001
+ }
1002
+
1003
+ /**
1004
+ * Delay helper for retry logic
1005
+ */
1006
+ private delay(ms: number): Promise<void> {
1007
+ return new Promise(resolve => setTimeout(resolve, ms));
1008
+ }
1009
+ }
1010
+ ```
1011
+
1012
+ ### Usage Example
1013
+
1014
+ ```bash
1015
+ # Start LDAP-Rest with notification plugin
1016
+ npx ldap-rest \
1017
+ --ldap-base dc=example,dc=com \
1018
+ --ldap-url ldap://localhost:389 \
1019
+ --ldap-dn cn=admin,dc=example,dc=com \
1020
+ --ldap-pwd admin \
1021
+ --plugin core/ldap/flatGeneric \
1022
+ --ldap-flat-schema ./static/schemas/standard/users.json \
1023
+ --plugin core/notification/webhook \
1024
+ --webhook-url https://example.com/webhook \
1025
+ --webhook-secret mysecret123 \
1026
+ --webhook-timeout 5000 \
1027
+ --webhook-retry-count 3 \
1028
+ --webhook-event ldap.add \
1029
+ --webhook-event ldap.modify
1030
+
1031
+ # Check webhook stats
1032
+ curl http://localhost:8081/api/v1/notifications/stats
1033
+
1034
+ # Test webhook
1035
+ curl -X POST http://localhost:8081/api/v1/notifications/test
1036
+ ```
1037
+
1038
+ ---
1039
+
1040
+ ## Testing Your Plugin
1041
+
1042
+ ### Test File Structure
1043
+
1044
+ Create tests in `test/plugins/<category>/<pluginName>.test.ts`:
1045
+
1046
+ ```typescript
1047
+ import { describe, it, before, after } from 'mocha';
1048
+ import { expect } from 'chai';
1049
+ import request from 'supertest';
1050
+
1051
+ import { DM } from '../../../src/bin/index';
1052
+ import NotificationPlugin from '../../../src/plugins/notification/webhook';
1053
+
1054
+ describe('NotificationPlugin', function () {
1055
+ let server: DM;
1056
+ let plugin: NotificationPlugin;
1057
+
1058
+ // Skip tests if environment not configured
1059
+ if (!process.env.DM_LDAP_DN || !process.env.DM_LDAP_PWD) {
1060
+ console.warn('Skipping NotificationPlugin tests: LDAP not configured');
1061
+ // @ts-ignore
1062
+ this.skip?.();
1063
+ return;
1064
+ }
1065
+
1066
+ before(async () => {
1067
+ // Create server instance
1068
+ server = new DM();
1069
+
1070
+ // Set up command-line args for plugin
1071
+ process.argv = [
1072
+ 'node',
1073
+ 'test',
1074
+ '--plugin',
1075
+ 'core/notification/webhook',
1076
+ '--webhook-url',
1077
+ 'http://localhost:9999/webhook',
1078
+ '--webhook-secret',
1079
+ 'test-secret',
1080
+ '--webhook-event',
1081
+ 'ldap.add',
1082
+ ];
1083
+
1084
+ await server.ready;
1085
+ await server.run();
1086
+
1087
+ // Get plugin instance
1088
+ plugin = server.loadedPlugins['notification'] as NotificationPlugin;
1089
+ });
1090
+
1091
+ after(async () => {
1092
+ if (server.server) {
1093
+ await new Promise(resolve => server.server!.close(resolve));
1094
+ }
1095
+ });
1096
+
1097
+ describe('Initialization', () => {
1098
+ it('should initialize with correct configuration', () => {
1099
+ expect(plugin).to.exist;
1100
+ expect(plugin.name).to.equal('notification');
1101
+ });
1102
+
1103
+ it('should throw error without required config', () => {
1104
+ expect(() => {
1105
+ const badServer = new DM();
1106
+ new NotificationPlugin(badServer);
1107
+ }).to.throw('requires --webhook-url');
1108
+ });
1109
+ });
1110
+
1111
+ describe('API Endpoints', () => {
1112
+ it('should return webhook stats', async () => {
1113
+ const response = await request(server.app)
1114
+ .get('/api/v1/notifications/stats')
1115
+ .expect(200);
1116
+
1117
+ expect(response.body).to.have.property('success', true);
1118
+ expect(response.body.data).to.have.property('sent');
1119
+ expect(response.body.data).to.have.property('failed');
1120
+ expect(response.body.data).to.have.property('webhookUrl');
1121
+ });
1122
+
1123
+ it('should send test webhook', async () => {
1124
+ const response = await request(server.app)
1125
+ .post('/api/v1/notifications/test')
1126
+ .expect(200);
1127
+
1128
+ expect(response.body).to.have.property('success', true);
1129
+ });
1130
+ });
1131
+
1132
+ describe('Hooks', () => {
1133
+ it('should trigger webhook on LDAP add', async () => {
1134
+ const testDn = `uid=testuser,${process.env.DM_LDAP_BASE}`;
1135
+
1136
+ // Add test entry
1137
+ await server.ldap.add(testDn, {
1138
+ objectClass: ['inetOrgPerson'],
1139
+ uid: 'testuser',
1140
+ cn: 'Test User',
1141
+ sn: 'User',
1142
+ });
1143
+
1144
+ // Wait for webhook
1145
+ await new Promise(resolve => setTimeout(resolve, 100));
1146
+
1147
+ // Check stats
1148
+ const response = await request(server.app)
1149
+ .get('/api/v1/notifications/stats')
1150
+ .expect(200);
1151
+
1152
+ expect(response.body.data.sent).to.be.greaterThan(0);
1153
+
1154
+ // Cleanup
1155
+ await server.ldap.delete(testDn);
1156
+ });
1157
+ });
1158
+
1159
+ describe('Private Methods', () => {
1160
+ it('should check enabled events correctly', () => {
1161
+ // Access private method via type assertion for testing
1162
+ const shouldSend = (plugin as any).shouldSendEvent('ldap.add');
1163
+ expect(shouldSend).to.be.true;
1164
+ });
1165
+ });
1166
+ });
1167
+ ```
1168
+
1169
+ ### Running Tests
1170
+
1171
+ ```bash
1172
+ # Set up test environment
1173
+ cat > ~/.test-env <<EOF
1174
+ export DM_LDAP_URL="ldap://localhost:389"
1175
+ export DM_LDAP_DN="cn=admin,dc=example,dc=com"
1176
+ export DM_LDAP_PWD="admin"
1177
+ export DM_LDAP_BASE="dc=example,dc=com"
1178
+ EOF
1179
+
1180
+ # Run all tests
1181
+ source ~/.test-env && npm test
1182
+
1183
+ # Run specific test file
1184
+ source ~/.test-env && npm run test:one test/plugins/notification/webhook.test.ts
1185
+
1186
+ # Run tests in watch mode
1187
+ source ~/.test-env && npm run test:dev
1188
+ ```
1189
+
1190
+ ### Test Best Practices
1191
+
1192
+ 1. **Use test helpers** from `test/helpers/` for common operations
1193
+ 2. **Clean up** - Delete test entries in `after()` hooks
1194
+ 3. **Skip gracefully** - Skip tests if environment not configured
1195
+ 4. **Mock external services** - Use mock servers for webhooks, APIs
1196
+ 5. **Test error cases** - Verify error handling and validation
1197
+ 6. **Test configuration** - Verify all config options work correctly
1198
+
1199
+ ---
1200
+
1201
+ ## Additional Resources
1202
+
1203
+ - [Contributing Guide](../../CONTRIBUTING.md) - Git workflow and PR guidelines
1204
+ - [Hooks Reference](../../HOOKS.md) - Complete list of available hooks
1205
+ - [Core Plugin Examples](../../src/plugins/) - Browse existing plugins
1206
+ - [Test Examples](../../test/plugins/) - See how core plugins are tested
1207
+
1208
+ ---
1209
+
1210
+ ## Support
1211
+
1212
+ - **Issues**: https://github.com/linagora/ldap-rest/issues
1213
+ - **Discussions**: https://github.com/linagora/ldap-rest/discussions
1214
+
1215
+ ---
1216
+
1217
+ ## License
1218
+
1219
+ By contributing plugins to LDAP-Rest, you agree that your contributions will be licensed under the AGPL-3.0 License.