@linagora/ldap-rest-client 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,1134 @@
1
+ // src/config/ClientConfig.ts
2
+ var ConfigValidator = class {
3
+ static validate(config) {
4
+ if (!config.baseUrl || config.baseUrl.trim().length === 0) {
5
+ throw new Error("baseUrl is required");
6
+ }
7
+ if (config.auth?.type === "hmac") {
8
+ if (!config.auth.serviceId || config.auth.serviceId.trim().length === 0) {
9
+ throw new Error("serviceId is required for HMAC authentication");
10
+ }
11
+ if (!config.auth.secret || config.auth.secret.trim().length === 0) {
12
+ throw new Error("secret is required for HMAC authentication");
13
+ }
14
+ if (config.auth.secret.length < 32) {
15
+ console.warn(
16
+ `[LDAP-REST Client] Secret should be at least 32 characters (current: ${config.auth.secret.length})`
17
+ );
18
+ }
19
+ }
20
+ try {
21
+ new URL(config.baseUrl);
22
+ } catch {
23
+ throw new Error("baseUrl must be a valid URL");
24
+ }
25
+ if (config.timeout !== void 0 && (config.timeout <= 0 || !Number.isFinite(config.timeout))) {
26
+ throw new Error("timeout must be a positive number");
27
+ }
28
+ }
29
+ static normalize(config) {
30
+ return {
31
+ baseUrl: config.baseUrl.replace(/\/$/, ""),
32
+ auth: config.auth ?? { type: "cookie" },
33
+ timeout: config.timeout ?? 3e4,
34
+ logger: config.logger
35
+ };
36
+ }
37
+ static toHttpConfig(config) {
38
+ return {
39
+ baseUrl: config.baseUrl,
40
+ timeout: config.timeout
41
+ };
42
+ }
43
+ };
44
+
45
+ // src/lib/HmacAuth.ts
46
+ import { createHmac, createHash } from "crypto";
47
+ var HmacAuth = class {
48
+ /**
49
+ * Creates an HMAC authentication handler
50
+ *
51
+ * @param {HmacAuthConfig} config - HMAC authentication configuration containing serviceId and secret
52
+ */
53
+ constructor(config) {
54
+ this.config = config;
55
+ }
56
+ /**
57
+ * Generates authorization header for a request
58
+ *
59
+ * @param {SignatureParams} params - Request parameters to sign
60
+ * @returns {string} Authorization header value in format "HMAC-SHA256 service-id:timestamp:signature"
61
+ */
62
+ sign = (params) => {
63
+ const signature = this.generateSignature(params);
64
+ return `HMAC-SHA256 ${signature.serviceId}:${signature.timestamp}:${signature.signature}`;
65
+ };
66
+ /**
67
+ * Generates HMAC signature components
68
+ *
69
+ * @param {SignatureParams} params - Request parameters
70
+ * @returns {HmacSignature} Signature components including service ID, timestamp, and signature
71
+ * @private
72
+ */
73
+ generateSignature = (params) => {
74
+ const timestamp = Date.now();
75
+ const bodyHash = this.hashBody(params.method, params.body);
76
+ const signingString = `${params.method.toUpperCase()}|${params.path}|${timestamp}|${bodyHash}`;
77
+ const signature = this.computeHmac(signingString);
78
+ return {
79
+ serviceId: this.config.serviceId,
80
+ timestamp,
81
+ signature
82
+ };
83
+ };
84
+ /**
85
+ * Computes SHA256 hash of request body
86
+ *
87
+ * For GET/DELETE/HEAD requests, returns empty string.
88
+ * For other methods, returns SHA256 hash of body if provided.
89
+ *
90
+ * @param {string} method - HTTP method
91
+ * @param {string} [body] - Request body
92
+ * @returns {string} SHA256 hash as hex string or empty string
93
+ * @private
94
+ */
95
+ hashBody = (method, body) => {
96
+ const upperMethod = method.toUpperCase();
97
+ if (upperMethod === "GET" || upperMethod === "DELETE" || upperMethod === "HEAD") {
98
+ return "";
99
+ }
100
+ if (!body || body.trim().length === 0) {
101
+ return "";
102
+ }
103
+ return createHash("sha256").update(body, "utf8").digest("hex");
104
+ };
105
+ /**
106
+ * Computes HMAC-SHA256 signature
107
+ *
108
+ * @param {string} data - Data to sign
109
+ * @returns {string} HMAC-SHA256 signature as hex string
110
+ * @private
111
+ */
112
+ computeHmac = (data) => {
113
+ return createHmac("sha256", this.config.secret).update(data, "utf8").digest("hex");
114
+ };
115
+ };
116
+
117
+ // src/errors/LdapRestError.ts
118
+ var LdapRestError = class extends Error {
119
+ /** Error class name (automatically set to constructor name) */
120
+ name;
121
+ /** HTTP status code associated with the error (if applicable) */
122
+ statusCode;
123
+ /** Machine-readable error code for programmatic handling */
124
+ code;
125
+ /**
126
+ * Creates a new LDAP-REST error
127
+ *
128
+ * @param {string} message - Human-readable error message
129
+ * @param {number} [statusCode] - HTTP status code
130
+ * @param {string} [code] - Machine-readable error code
131
+ */
132
+ constructor(message, statusCode, code) {
133
+ super(message);
134
+ this.name = this.constructor.name;
135
+ this.statusCode = statusCode;
136
+ this.code = code;
137
+ Error.captureStackTrace(this, this.constructor);
138
+ }
139
+ };
140
+
141
+ // src/errors/ApiError.ts
142
+ var ApiError = class _ApiError extends LdapRestError {
143
+ /**
144
+ * Creates a new API error
145
+ *
146
+ * @param {string} message - Human-readable error message
147
+ * @param {number} statusCode - HTTP status code
148
+ * @param {string} code - Machine-readable error code
149
+ */
150
+ constructor(message, statusCode, code) {
151
+ super(message, statusCode, code);
152
+ }
153
+ /**
154
+ * Creates an ApiError from an API response body
155
+ *
156
+ * @param {number} statusCode - HTTP status code from response
157
+ * @param {{ error: string; code: string }} body - Response body containing error details
158
+ * @returns {ApiError} New ApiError instance
159
+ */
160
+ static fromResponse(statusCode, body) {
161
+ return new _ApiError(body.error, statusCode, body.code);
162
+ }
163
+ };
164
+ var ValidationError = class extends LdapRestError {
165
+ /**
166
+ * Creates a new validation error
167
+ *
168
+ * @param {string} message - Description of what validation failed
169
+ */
170
+ constructor(message) {
171
+ super(message, 400, "VALIDATION_ERROR");
172
+ }
173
+ };
174
+ var AuthenticationError = class extends LdapRestError {
175
+ /**
176
+ * Creates a new authentication error
177
+ *
178
+ * @param {string} message - Authentication failure reason
179
+ */
180
+ constructor(message) {
181
+ super(message, 401, "AUTHENTICATION_ERROR");
182
+ }
183
+ };
184
+ var AuthorizationError = class extends LdapRestError {
185
+ /**
186
+ * Creates a new authorization error
187
+ *
188
+ * @param {string} message - Authorization failure reason
189
+ */
190
+ constructor(message) {
191
+ super(message, 403, "AUTHORIZATION_ERROR");
192
+ }
193
+ };
194
+ var NotFoundError = class extends LdapRestError {
195
+ /**
196
+ * Creates a new not found error
197
+ *
198
+ * @param {string} message - Description of what was not found
199
+ * @param {string} [code] - Optional specific error code (defaults to 'NOT_FOUND')
200
+ */
201
+ constructor(message, code) {
202
+ super(message, 404, code || "NOT_FOUND");
203
+ }
204
+ };
205
+ var ConflictError = class extends LdapRestError {
206
+ /**
207
+ * Creates a new conflict error
208
+ *
209
+ * @param {string} message - Description of the conflict
210
+ * @param {string} [code] - Optional specific error code (e.g., 'USERNAME_EXISTS', 'EMAIL_EXISTS')
211
+ */
212
+ constructor(message, code) {
213
+ super(message, 409, code || "CONFLICT");
214
+ }
215
+ };
216
+ var RateLimitError = class extends LdapRestError {
217
+ /** Number of seconds to wait before retrying (from Retry-After header) */
218
+ retryAfter;
219
+ /**
220
+ * Creates a new rate limit error
221
+ *
222
+ * @param {string} message - Rate limit error message
223
+ * @param {number} [retryAfter] - Seconds to wait before retrying
224
+ */
225
+ constructor(message, retryAfter) {
226
+ super(message, 429, "RATE_LIMIT_EXCEEDED");
227
+ this.retryAfter = retryAfter;
228
+ }
229
+ };
230
+ var NetworkError = class extends LdapRestError {
231
+ /** Original error that caused the network failure */
232
+ cause;
233
+ /**
234
+ * Creates a new network error
235
+ *
236
+ * @param {string} message - Network error description
237
+ * @param {Error} [cause] - Original error that caused the failure
238
+ */
239
+ constructor(message, cause) {
240
+ super(message);
241
+ this.cause = cause;
242
+ }
243
+ };
244
+
245
+ // src/lib/HttpClient.ts
246
+ var HttpClient = class {
247
+ /**
248
+ * Creates an HTTP client instance
249
+ *
250
+ * @param {HttpConfig} config - HTTP configuration (baseUrl, timeout)
251
+ * @param {Auth | undefined} auth - Optional authentication handler (HMAC). If undefined, uses cookies.
252
+ * @param {Logger<unknown>} logger - Logger instance
253
+ */
254
+ constructor(config, auth, logger) {
255
+ this.config = config;
256
+ this.auth = auth;
257
+ this.logger = logger;
258
+ }
259
+ /**
260
+ * Makes an authenticated HTTP request
261
+ *
262
+ * @template T - Response type
263
+ * @param {RequestOptions} options - Request options
264
+ * @returns {Promise<T>} Parsed response body
265
+ * @throws {ApiError} When API returns an error
266
+ * @throws {NetworkError} When network request fails
267
+ */
268
+ request = async (options) => {
269
+ const url = `${this.config.baseUrl}${options.path}`;
270
+ const bodyString = options.body ? JSON.stringify(options.body) : void 0;
271
+ this.logger.debug("Sending request", {
272
+ method: options.method,
273
+ url,
274
+ hasBody: !!bodyString
275
+ });
276
+ const authHeader = this.auth?.sign({
277
+ method: options.method,
278
+ path: options.path,
279
+ body: bodyString
280
+ });
281
+ const headers = {
282
+ "Content-Type": "application/json",
283
+ ...options.headers
284
+ };
285
+ if (authHeader) {
286
+ headers.Authorization = authHeader;
287
+ }
288
+ const controller = new AbortController();
289
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
290
+ try {
291
+ const response = await fetch(url, {
292
+ method: options.method,
293
+ headers,
294
+ body: bodyString,
295
+ signal: controller.signal,
296
+ credentials: authHeader ? void 0 : "include"
297
+ });
298
+ clearTimeout(timeoutId);
299
+ this.logger.info("Request completed", {
300
+ method: options.method,
301
+ path: options.path,
302
+ status: response.status
303
+ });
304
+ return this.handleResponse(response);
305
+ } catch (error) {
306
+ clearTimeout(timeoutId);
307
+ if (error instanceof Error && error.name === "AbortError") {
308
+ this.logger.error("Request timeout", {
309
+ method: options.method,
310
+ path: options.path,
311
+ timeoutMs: this.config.timeout
312
+ });
313
+ throw new NetworkError(`Request timeout after ${this.config.timeout}ms`);
314
+ }
315
+ if (error instanceof NetworkError || error instanceof ApiError) {
316
+ throw error;
317
+ }
318
+ this.logger.error("Network request failed", {
319
+ method: options.method,
320
+ path: options.path,
321
+ error: error instanceof Error ? error.message : "Unknown error"
322
+ });
323
+ throw new NetworkError(
324
+ `Network request failed: ${error instanceof Error ? error.message : "Unknown error"}`,
325
+ error instanceof Error ? error : void 0
326
+ );
327
+ }
328
+ };
329
+ /**
330
+ * Handles HTTP response
331
+ *
332
+ * Maps HTTP status codes to specific error types and parses response body.
333
+ *
334
+ * @template T - Response type
335
+ * @param {Response} response - Fetch API response
336
+ * @returns {Promise<T>} Parsed response body
337
+ * @throws {ApiError} When API returns an error status
338
+ * @private
339
+ */
340
+ handleResponse = async (response) => {
341
+ if (response.ok) {
342
+ if (response.status === 204) {
343
+ return { success: true };
344
+ }
345
+ const contentType = response.headers.get("content-type");
346
+ if (!contentType?.includes("application/json")) {
347
+ throw new ApiError("Expected JSON response", response.status, "INVALID_RESPONSE");
348
+ }
349
+ return response.json();
350
+ }
351
+ let errorBody;
352
+ try {
353
+ const json = await response.json();
354
+ errorBody = json;
355
+ } catch {
356
+ }
357
+ const message = errorBody?.error ?? `HTTP ${response.status}: ${response.statusText}`;
358
+ const code = errorBody?.code ?? "UNKNOWN_ERROR";
359
+ switch (response.status) {
360
+ case 400:
361
+ throw new ValidationError(message);
362
+ case 401:
363
+ throw new AuthenticationError(message);
364
+ case 403:
365
+ throw new AuthorizationError(message);
366
+ case 404:
367
+ throw new NotFoundError(message, code);
368
+ case 409:
369
+ throw new ConflictError(message, code);
370
+ case 429: {
371
+ const retryAfter = response.headers.get("retry-after");
372
+ throw new RateLimitError(message, retryAfter ? parseInt(retryAfter, 10) : void 0);
373
+ }
374
+ default:
375
+ if (errorBody) {
376
+ throw ApiError.fromResponse(response.status, errorBody);
377
+ }
378
+ throw new ApiError(message, response.status, code);
379
+ }
380
+ };
381
+ /**
382
+ * Performs a GET request
383
+ *
384
+ * @template T - Response type
385
+ * @param {string} path - Request path
386
+ * @param {Record<string, string>} [headers] - Additional headers
387
+ * @returns {Promise<T>} Parsed response body
388
+ */
389
+ get = (path, headers) => {
390
+ return this.request({ method: "GET", path, headers });
391
+ };
392
+ /**
393
+ * Performs a POST request
394
+ *
395
+ * @template T - Response type
396
+ * @param {string} path - Request path
397
+ * @param {unknown} [body] - Request body
398
+ * @param {Record<string, string>} [headers] - Additional headers
399
+ * @returns {Promise<T>} Parsed response body
400
+ */
401
+ post = (path, body, headers) => {
402
+ return this.request({ method: "POST", path, body, headers });
403
+ };
404
+ /**
405
+ * Performs a PATCH request
406
+ *
407
+ * @template T - Response type
408
+ * @param {string} path - Request path
409
+ * @param {unknown} [body] - Request body
410
+ * @param {Record<string, string>} [headers] - Additional headers
411
+ * @returns {Promise<T>} Parsed response body
412
+ */
413
+ patch = (path, body, headers) => {
414
+ return this.request({ method: "PATCH", path, body, headers });
415
+ };
416
+ /**
417
+ * Performs a DELETE request
418
+ *
419
+ * @template T - Response type
420
+ * @param {string} path - Request path
421
+ * @param {Record<string, string>} [headers] - Additional headers
422
+ * @returns {Promise<T>} Parsed response body
423
+ */
424
+ delete = (path, headers) => {
425
+ return this.request({ method: "DELETE", path, headers });
426
+ };
427
+ };
428
+
429
+ // src/lib/index.ts
430
+ import { Logger } from "tslog";
431
+
432
+ // src/LdapRestClient.ts
433
+ import { Logger as Logger2 } from "tslog";
434
+
435
+ // src/resources/BaseResource.ts
436
+ var BaseResource = class {
437
+ /**
438
+ * Creates a base resource instance
439
+ *
440
+ * @param {HttpClient} http - HTTP client for making requests
441
+ * @protected
442
+ */
443
+ constructor(http) {
444
+ this.http = http;
445
+ }
446
+ /**
447
+ * Builds a query string from parameters
448
+ *
449
+ * Filters out undefined and null values, and properly encodes
450
+ * parameter names and values for URL use.
451
+ *
452
+ * @param {Record<string, string | number | boolean | undefined>} params - Query parameters
453
+ * @returns {string} Formatted query string with leading '?' or empty string
454
+ * @protected
455
+ *
456
+ * @example
457
+ * ```typescript
458
+ * buildQueryString({ field: 'username', value: 'john' })
459
+ * // Returns: "?field=username&value=john"
460
+ * ```
461
+ */
462
+ buildQueryString = (params) => {
463
+ const entries = Object.entries(params).filter(
464
+ ([_, value]) => value !== void 0 && value !== null
465
+ );
466
+ if (entries.length === 0) {
467
+ return "";
468
+ }
469
+ const queryParams = entries.map(
470
+ ([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`
471
+ );
472
+ return `?${queryParams.join("&")}`;
473
+ };
474
+ };
475
+
476
+ // src/resources/UsersResource.ts
477
+ var UsersResource = class extends BaseResource {
478
+ /**
479
+ * Creates a new user in the main LDAP branch
480
+ *
481
+ * @param {CreateUserRequest} data - User data including credentials and profile
482
+ * @returns {Promise<{ success: true }>} Success response
483
+ * @throws {ConflictError} When username/email/phone already exists
484
+ * @throws {ApiError} On other API errors
485
+ */
486
+ create = async (data) => {
487
+ return this.http.post("/api/v1/users", data);
488
+ };
489
+ /**
490
+ * Updates an existing user
491
+ *
492
+ * @param {string} userId - User identifier (username)
493
+ * @param {UpdateUserRequest} data - Fields to update
494
+ * @returns {Promise<{ success: true }>} Success response
495
+ * @throws {NotFoundError} When user is not found
496
+ * @throws {ApiError} On other API errors
497
+ */
498
+ update = async (userId, data) => {
499
+ return this.http.patch(`/api/v1/users/${encodeURIComponent(userId)}`, data);
500
+ };
501
+ /**
502
+ * Disables a user account
503
+ *
504
+ * Sets pwdAccountLockedTime to lock the account using LDAP PPolicy.
505
+ *
506
+ * @param {string} userId - User identifier (username)
507
+ * @returns {Promise<{ success: true }>} Success response
508
+ * @throws {NotFoundError} When user is not found
509
+ * @throws {ApiError} On other API errors
510
+ */
511
+ disable = async (userId) => {
512
+ return this.http.post(`/api/v1/users/${encodeURIComponent(userId)}/disable`);
513
+ };
514
+ /**
515
+ * Deletes a user
516
+ *
517
+ * Permanently removes the user from LDAP.
518
+ *
519
+ * @param {string} userId - User identifier (username)
520
+ * @returns {Promise<{ success: true }>} Success response
521
+ * @throws {NotFoundError} When user is not found
522
+ * @throws {ApiError} On other API errors
523
+ */
524
+ delete = async (userId) => {
525
+ return this.http.delete(`/api/v1/users/${encodeURIComponent(userId)}`);
526
+ };
527
+ /**
528
+ * Checks if a username, phone, or email is available
529
+ *
530
+ * @param {CheckAvailabilityParams} params - Field and value to check
531
+ * @returns {Promise<CheckAvailabilityResponse>} Availability status
532
+ * @throws {ApiError} On API errors
533
+ *
534
+ * @example
535
+ * ```typescript
536
+ * const result = await client.users.checkAvailability({
537
+ * field: 'username',
538
+ * value: 'johndoe'
539
+ * });
540
+ *
541
+ * ```
542
+ */
543
+ checkAvailability = async (params) => {
544
+ const query = this.buildQueryString(params);
545
+ return this.http.get(`/api/v1/users/check${query}`);
546
+ };
547
+ /**
548
+ * Fetches a user by identifier
549
+ *
550
+ * @param {FetchUserRequest} params - Fetch parameters (by, value, fields)
551
+ * @returns {Promise<User>} User data
552
+ * @throws {NotFoundError} When user is not found
553
+ * @throws {ApiError} On other API errors
554
+ *
555
+ * @example
556
+ * ```typescript
557
+ * const user = await client.users.fetch({
558
+ * by: 'username',
559
+ * value: 'johndoe',
560
+ * fields: 'cn,mail,mobile'
561
+ * });
562
+ * ```
563
+ */
564
+ fetch = async (params) => {
565
+ const query = this.buildQueryString(params);
566
+ return this.http.get(`/api/v1/users${query}`);
567
+ };
568
+ };
569
+
570
+ // src/resources/OrganizationsResource.ts
571
+ var OrganizationsResource = class extends BaseResource {
572
+ /**
573
+ * Creates a new organization with dedicated LDAP branch
574
+ *
575
+ * @param {CreateOrganizationRequest} data - Organization data
576
+ * @returns {Promise<CreateOrganizationResponse>} Organization details including baseDN
577
+ * @throws {ConflictError} When organization already exists
578
+ * @throws {ApiError} On other API errors
579
+ */
580
+ create = async (data) => {
581
+ return this.http.post("/api/v1/organizations", data);
582
+ };
583
+ /**
584
+ * Checks if an organization identifier is available
585
+ *
586
+ * @param {CheckAvailabilityParams} params - Field and value to check
587
+ * @returns {Promise<CheckAvailabilityResponse>} Availability status
588
+ * @throws {ApiError} On API errors
589
+ *
590
+ * @example
591
+ * ```typescript
592
+ * const result = await client.organizations.checkAvailability({
593
+ * field: 'domain',
594
+ * value: 'acme.example.com'
595
+ * });
596
+ * ```
597
+ */
598
+ checkAvailability = async (params) => {
599
+ const query = this.buildQueryString(params);
600
+ return this.http.get(`/api/v1/organizations/check${query}`);
601
+ };
602
+ /**
603
+ * Links a user as the first admin of an organization
604
+ *
605
+ * Associates an existing user account with an organization and grants
606
+ * admin privileges. Typically called after organization creation.
607
+ *
608
+ * @param {string} organizationId - Organization identifier
609
+ * @param {CreateAdminRequest} data - Admin user details (username and email)
610
+ * @returns {Promise<{ success: true }>} Success response
611
+ * @throws {NotFoundError} When organization or user is not found
612
+ * @throws {ConflictError} When user is already an admin
613
+ * @throws {ApiError} On other API errors
614
+ *
615
+ * @example
616
+ * ```typescript
617
+ * await client.organizations.createAdmin('org_abc123', {
618
+ * username: 'john.doe',
619
+ * mail: 'john.doe@acme.example.com'
620
+ * });
621
+ * ```
622
+ */
623
+ createAdmin = async (organizationId, data) => {
624
+ return this.http.post(
625
+ `/api/v1/organizations/${encodeURIComponent(organizationId)}/admin`,
626
+ data
627
+ );
628
+ };
629
+ /**
630
+ * Gets a list of organizations for the authenticated user
631
+ *
632
+ * Requires SSO cookie authentication. Returns organizations where the
633
+ * authenticated user has admin role.
634
+ *
635
+ * @returns {Promise<Organization[]>} Array of organizations
636
+ * @throws {ApiError} On API errors
637
+ *
638
+ * @example
639
+ * ```typescript
640
+ * const organizations = await client.organizations.list();
641
+ * ```
642
+ */
643
+ list = async () => {
644
+ return this.http.get("/api/v1/organizations");
645
+ };
646
+ /**
647
+ * Gets details of a specific organization
648
+ *
649
+ * Requires SSO cookie authentication.
650
+ *
651
+ * @param {string} organizationId - Organization identifier
652
+ * @returns {Promise<Organization>} Organization details
653
+ * @throws {NotFoundError} When organization is not found
654
+ * @throws {ApiError} On other API errors
655
+ *
656
+ * @example
657
+ * ```typescript
658
+ * const org = await client.organizations.get('org_abc123');
659
+ * ```
660
+ */
661
+ get = async (organizationId) => {
662
+ return this.http.get(`/api/v1/organizations/${encodeURIComponent(organizationId)}`);
663
+ };
664
+ /**
665
+ * Updates an organization
666
+ *
667
+ * Requires SSO cookie authentication and admin role.
668
+ *
669
+ * @param {string} organizationId - Organization identifier
670
+ * @param {UpdateOrganizationRequest} data - Fields to update
671
+ * @returns {Promise<{ success: true }>} Success response
672
+ * @throws {NotFoundError} When organization is not found
673
+ * @throws {ForbiddenError} When user lacks admin privileges
674
+ * @throws {ApiError} On other API errors
675
+ *
676
+ * @example
677
+ * ```typescript
678
+ * await client.organizations.update('org_abc123', {
679
+ * name: 'New Organization Name',
680
+ * status: 'active'
681
+ * });
682
+ * ```
683
+ */
684
+ update = async (organizationId, data) => {
685
+ return this.http.patch(`/api/v1/organizations/${encodeURIComponent(organizationId)}`, data);
686
+ };
687
+ // ===== B2B User Management Methods =====
688
+ /**
689
+ * Creates a new user in an organization's LDAP branch
690
+ *
691
+ * Requires SSO cookie authentication and admin role in the target organization.
692
+ *
693
+ * @param {string} organizationId - Organization identifier
694
+ * @param {CreateUserRequest} data - User data including credentials and profile
695
+ * @returns {Promise<CreateB2BUserResponse>} Response with user's baseDN
696
+ * @throws {ForbiddenError} When user lacks admin privileges
697
+ * @throws {NotFoundError} When organization is not found
698
+ * @throws {ConflictError} When username/email/phone already exists
699
+ * @throws {ApiError} On other API errors
700
+ *
701
+ * @example
702
+ * ```typescript
703
+ * const result = await client.organizations.createUser('org_abc123', {
704
+ * cn: 'john.doe',
705
+ * uid: 'john.doe',
706
+ * // ... other user fields
707
+ * });
708
+ * ```
709
+ */
710
+ createUser = async (organizationId, data) => {
711
+ return this.http.post(
712
+ `/api/v1/organizations/${encodeURIComponent(organizationId)}/users`,
713
+ data
714
+ );
715
+ };
716
+ /**
717
+ * Updates a user in an organization
718
+ *
719
+ * Requires SSO cookie authentication and admin role in the target organization.
720
+ *
721
+ * @param {string} organizationId - Organization identifier
722
+ * @param {string} userId - User identifier (username)
723
+ * @param {UpdateUserRequest} data - Fields to update
724
+ * @returns {Promise<{ success: true }>} Success response
725
+ * @throws {NotFoundError} When user or organization is not found
726
+ * @throws {ForbiddenError} When user lacks admin privileges
727
+ * @throws {ApiError} On other API errors
728
+ *
729
+ * @example
730
+ * ```typescript
731
+ * await client.organizations.updateUser('org_abc123', 'john.doe', {
732
+ * mobile: '+33687654321'
733
+ * });
734
+ * ```
735
+ */
736
+ updateUser = async (organizationId, userId, data) => {
737
+ return this.http.patch(
738
+ `/api/v1/organizations/${encodeURIComponent(organizationId)}/users/${encodeURIComponent(userId)}`,
739
+ data
740
+ );
741
+ };
742
+ /**
743
+ * Disables a user in an organization
744
+ *
745
+ * Locks the account by setting pwdAccountLockedTime using LDAP PPolicy.
746
+ * Requires SSO cookie authentication and admin role.
747
+ *
748
+ * @param {string} organizationId - Organization identifier
749
+ * @param {string} userId - User identifier (username)
750
+ * @returns {Promise<{ success: true }>} Success response
751
+ * @throws {NotFoundError} When user or organization is not found
752
+ * @throws {ForbiddenError} When user lacks admin privileges
753
+ * @throws {ApiError} On other API errors
754
+ *
755
+ * @example
756
+ * ```typescript
757
+ * await client.organizations.disableUser('org_abc123', 'john.doe');
758
+ * ```
759
+ */
760
+ disableUser = async (organizationId, userId) => {
761
+ return this.http.post(
762
+ `/api/v1/organizations/${encodeURIComponent(organizationId)}/users/${encodeURIComponent(userId)}/disable`
763
+ );
764
+ };
765
+ /**
766
+ * Deletes a user from an organization
767
+ *
768
+ * Permanently removes the user from the organization's LDAP branch.
769
+ * Requires SSO cookie authentication and admin role.
770
+ *
771
+ * @param {string} organizationId - Organization identifier
772
+ * @param {string} userId - User identifier (username)
773
+ * @returns {Promise<{ success: true }>} Success response
774
+ * @throws {NotFoundError} When user or organization is not found
775
+ * @throws {ForbiddenError} When user lacks admin privileges
776
+ * @throws {ApiError} On other API errors
777
+ *
778
+ * @example
779
+ * ```typescript
780
+ * await client.organizations.deleteUser('org_abc123', 'john.doe');
781
+ * ```
782
+ */
783
+ deleteUser = async (organizationId, userId) => {
784
+ return this.http.delete(
785
+ `/api/v1/organizations/${encodeURIComponent(organizationId)}/users/${encodeURIComponent(userId)}`
786
+ );
787
+ };
788
+ /**
789
+ * Fetches a user from an organization by identifier
790
+ *
791
+ * Requires SSO cookie authentication and admin role.
792
+ *
793
+ * @param {string} organizationId - Organization identifier
794
+ * @param {FetchUserRequest} params - Fetch parameters (by, value, fields)
795
+ * @returns {Promise<User>} User data
796
+ * @throws {NotFoundError} When user or organization is not found
797
+ * @throws {ForbiddenError} When user lacks admin privileges
798
+ * @throws {ApiError} On other API errors
799
+ *
800
+ * @example
801
+ * ```typescript
802
+ * const user = await client.organizations.getUser('org_abc123', {
803
+ * by: 'username',
804
+ * value: 'john.doe',
805
+ * fields: 'cn,mail,mobile'
806
+ * });
807
+ * ```
808
+ */
809
+ getUser = async (organizationId, params) => {
810
+ const query = this.buildQueryString(params);
811
+ return this.http.get(
812
+ `/api/v1/organizations/${encodeURIComponent(organizationId)}/users${query}`
813
+ );
814
+ };
815
+ /**
816
+ * Lists users in an organization with pagination and filtering
817
+ *
818
+ * Requires SSO cookie authentication and admin role.
819
+ *
820
+ * @param {string} organizationId - Organization identifier
821
+ * @param {ListUsersParams} params - Pagination and filter parameters
822
+ * @returns {Promise<ListUsersResponse>} Paginated list of users
823
+ * @throws {NotFoundError} When organization is not found
824
+ * @throws {ForbiddenError} When user lacks admin privileges
825
+ * @throws {ApiError} On other API errors
826
+ *
827
+ * @example
828
+ * ```typescript
829
+ * const result = await client.organizations.listUsers('org_abc123', {
830
+ * page: 1,
831
+ * limit: 20,
832
+ * status: 'active',
833
+ * search: 'john',
834
+ * sortBy: 'createdAt',
835
+ * sortOrder: 'desc'
836
+ * });
837
+ * ```
838
+ */
839
+ listUsers = async (organizationId, params) => {
840
+ const query = params ? this.buildQueryString(params) : "";
841
+ return this.http.get(
842
+ `/api/v1/organizations/${encodeURIComponent(organizationId)}/users${query}`
843
+ );
844
+ };
845
+ /**
846
+ * Checks if a username, phone, or email is available in an organization
847
+ *
848
+ * Requires SSO cookie authentication and admin role.
849
+ *
850
+ * @param {string} organizationId - Organization identifier
851
+ * @param {CheckAvailabilityParams} params - Field and value to check
852
+ * @returns {Promise<CheckAvailabilityResponse>} Availability status
853
+ * @throws {NotFoundError} When organization is not found
854
+ * @throws {ForbiddenError} When user lacks admin privileges
855
+ * @throws {ApiError} On other API errors
856
+ *
857
+ * @example
858
+ * ```typescript
859
+ * const result = await client.organizations.checkUserAvailability('org_abc123', {
860
+ * field: 'username',
861
+ * value: 'john.doe'
862
+ * });
863
+ * ```
864
+ */
865
+ checkUserAvailability = async (organizationId, params) => {
866
+ const query = this.buildQueryString(params);
867
+ return this.http.get(
868
+ `/api/v1/organizations/${encodeURIComponent(organizationId)}/users/check${query}`
869
+ );
870
+ };
871
+ /**
872
+ * Changes a user's role in an organization
873
+ *
874
+ * Available roles: admin, moderator, member.
875
+ * Requires SSO cookie authentication and admin role.
876
+ * Cannot change your own role or remove the last admin.
877
+ *
878
+ * @param {string} organizationId - Organization identifier
879
+ * @param {string} userId - User identifier (username)
880
+ * @param {ChangeUserRoleRequest} data - New role
881
+ * @returns {Promise<{ success: true }>} Success response
882
+ * @throws {NotFoundError} When user or organization is not found
883
+ * @throws {ForbiddenError} When attempting self-demotion or removing last admin
884
+ * @throws {ApiError} On other API errors
885
+ *
886
+ * @example
887
+ * ```typescript
888
+ * await client.organizations.changeUserRole('org_abc123', 'john.doe', {
889
+ * role: 'moderator'
890
+ * });
891
+ * ```
892
+ */
893
+ changeUserRole = async (organizationId, userId, data) => {
894
+ return this.http.patch(
895
+ `/api/v1/organizations/${encodeURIComponent(organizationId)}/users/${encodeURIComponent(userId)}/role`,
896
+ data
897
+ );
898
+ };
899
+ };
900
+
901
+ // src/resources/GroupsResource.ts
902
+ var GroupsResource = class extends BaseResource {
903
+ /**
904
+ * Creates a new group in an organization
905
+ *
906
+ * Requires SSO cookie authentication and admin role in the target organization.
907
+ *
908
+ * @param {string} organizationId - Organization identifier
909
+ * @param {CreateGroupRequest} data - Group data (name and optional description)
910
+ * @returns {Promise<CreateGroupResponse>} Created group details
911
+ * @throws {ForbiddenError} When user lacks admin privileges
912
+ * @throws {NotFoundError} When organization is not found
913
+ * @throws {ConflictError} When group name already exists
914
+ * @throws {ApiError} On other API errors
915
+ *
916
+ * @examples
917
+ * ```typescript
918
+ * const result = await client.groups.create('org_abc123', {
919
+ * name: 'engineering',
920
+ * description: 'Engineering team'
921
+ * });
922
+ * ```
923
+ */
924
+ create = async (organizationId, data) => {
925
+ return this.http.post(
926
+ `/api/v1/organizations/${encodeURIComponent(organizationId)}/groups`,
927
+ data
928
+ );
929
+ };
930
+ /**
931
+ * Lists all groups in an organization with pagination
932
+ *
933
+ * Requires SSO cookie authentication and admin role.
934
+ *
935
+ * @param {string} organizationId - Organization identifier
936
+ * @param {ListGroupsParams} params - Pagination parameters (page, limit)
937
+ * @returns {Promise<ListGroupsResponse>} Paginated list of groups
938
+ * @throws {NotFoundError} When organization is not found
939
+ * @throws {ForbiddenError} When user lacks admin privileges
940
+ * @throws {ApiError} On other API errors
941
+ *
942
+ * @example
943
+ * ```typescript
944
+ * const result = await client.groups.list('org_abc123', {
945
+ * page: 1,
946
+ * limit: 20
947
+ * });
948
+ * ```
949
+ */
950
+ list = async (organizationId, params) => {
951
+ const query = params ? this.buildQueryString(params) : "";
952
+ return this.http.get(
953
+ `/api/v1/organizations/${encodeURIComponent(organizationId)}/groups${query}`
954
+ );
955
+ };
956
+ /**
957
+ * Gets details of a specific group
958
+ *
959
+ * Requires SSO cookie authentication and admin role.
960
+ *
961
+ * @param {string} organizationId - Organization identifier
962
+ * @param {string} groupId - Group identifier
963
+ * @returns {Promise<Group>} Group details including members
964
+ * @throws {NotFoundError} When organization or group is not found
965
+ * @throws {ForbiddenError} When user lacks admin privileges
966
+ * @throws {ApiError} On other API errors
967
+ *
968
+ * @example
969
+ * ```typescript
970
+ * const group = await client.groups.get('org_abc123', 'grp_xyz789');
971
+ * ```
972
+ */
973
+ get = async (organizationId, groupId) => {
974
+ return this.http.get(
975
+ `/api/v1/organizations/${encodeURIComponent(organizationId)}/groups/${encodeURIComponent(groupId)}`
976
+ );
977
+ };
978
+ /**
979
+ * Updates a group's properties
980
+ *
981
+ * Requires SSO cookie authentication and admin role.
982
+ *
983
+ * @param {string} organizationId - Organization identifier
984
+ * @param {string} groupId - Group identifier
985
+ * @param {UpdateGroupRequest} data - Fields to update (name, description)
986
+ * @returns {Promise<{ success: true }>} Success response
987
+ * @throws {NotFoundError} When organization or group is not found
988
+ * @throws {ForbiddenError} When user lacks admin privileges
989
+ * @throws {ConflictError} When new group name already exists
990
+ * @throws {ApiError} On other API errors
991
+ *
992
+ * @example
993
+ * ```typescript
994
+ * await client.groups.update('org_abc123', 'grp_xyz789', {
995
+ * description: 'Updated description'
996
+ * });
997
+ * ```
998
+ */
999
+ update = async (organizationId, groupId, data) => {
1000
+ return this.http.patch(
1001
+ `/api/v1/organizations/${encodeURIComponent(organizationId)}/groups/${encodeURIComponent(groupId)}`,
1002
+ data
1003
+ );
1004
+ };
1005
+ /**
1006
+ * Deletes a group from an organization
1007
+ *
1008
+ * Permanently removes the group. Members are not deleted, only the group itself.
1009
+ * Requires SSO cookie authentication and admin role.
1010
+ *
1011
+ * @param {string} organizationId - Organization identifier
1012
+ * @param {string} groupId - Group identifier
1013
+ * @returns {Promise<{ success: true }>} Success response
1014
+ * @throws {NotFoundError} When organization or group is not found
1015
+ * @throws {ForbiddenError} When user lacks admin privileges
1016
+ * @throws {ApiError} On other API errors
1017
+ *
1018
+ * @example
1019
+ * ```typescript
1020
+ * await client.groups.delete('org_abc123', 'grp_xyz789');
1021
+ * ```
1022
+ */
1023
+ delete = async (organizationId, groupId) => {
1024
+ return this.http.delete(
1025
+ `/api/v1/organizations/${encodeURIComponent(organizationId)}/groups/${encodeURIComponent(groupId)}`
1026
+ );
1027
+ };
1028
+ /**
1029
+ * Adds users to a group
1030
+ *
1031
+ * Adds one or more users to the group's member list.
1032
+ * Requires SSO cookie authentication and admin role.
1033
+ *
1034
+ * @param {string} organizationId - Organization identifier
1035
+ * @param {string} groupId - Group identifier
1036
+ * @param {AddGroupMembersRequest} data - Usernames to add
1037
+ * @returns {Promise<{ success: true }>} Success response
1038
+ * @throws {NotFoundError} When organization, group, or users are not found
1039
+ * @throws {ForbiddenError} When user lacks admin privileges
1040
+ * @throws {ApiError} On other API errors
1041
+ *
1042
+ * @example
1043
+ * ```typescript
1044
+ * await client.groups.addMembers('org_abc123', 'grp_xyz789', {
1045
+ * usernames: ['alice.johnson', 'bob.wilson']
1046
+ * });
1047
+ * ```
1048
+ */
1049
+ addMembers = async (organizationId, groupId, data) => {
1050
+ return this.http.post(
1051
+ `/api/v1/organizations/${encodeURIComponent(organizationId)}/groups/${encodeURIComponent(groupId)}/members`,
1052
+ data
1053
+ );
1054
+ };
1055
+ /**
1056
+ * Removes a user from a group
1057
+ *
1058
+ * Removes a specific user from the group's member list.
1059
+ * Requires SSO cookie authentication and admin role.
1060
+ *
1061
+ * @param {string} organizationId - Organization identifier
1062
+ * @param {string} groupId - Group identifier
1063
+ * @param {string} userId - User identifier (username) to remove
1064
+ * @returns {Promise<{ success: true }>} Success response
1065
+ * @throws {NotFoundError} When organization, group, or user is not found
1066
+ * @throws {ForbiddenError} When user lacks admin privileges
1067
+ * @throws {ApiError} On other API errors
1068
+ *
1069
+ * @example
1070
+ * ```typescript
1071
+ * await client.groups.removeMember('org_abc123', 'grp_xyz789', 'bob.wilson');
1072
+ * ```
1073
+ */
1074
+ removeMember = async (organizationId, groupId, userId) => {
1075
+ return this.http.delete(
1076
+ `/api/v1/organizations/${encodeURIComponent(organizationId)}/groups/${encodeURIComponent(groupId)}/members/${encodeURIComponent(userId)}`
1077
+ );
1078
+ };
1079
+ };
1080
+
1081
+ // src/LdapRestClient.ts
1082
+ var LdapRestClient = class {
1083
+ users;
1084
+ organizations;
1085
+ groups;
1086
+ config;
1087
+ /**
1088
+ * Creates a new LDAP-REST client instance
1089
+ *
1090
+ * @param {ClientConfig} config - Client configuration
1091
+ * @throws {Error} When configuration is invalid
1092
+ */
1093
+ constructor(config) {
1094
+ ConfigValidator.validate(config);
1095
+ this.config = ConfigValidator.normalize(config);
1096
+ const logger = new Logger2({
1097
+ name: "LDAP-REST-CLIENT",
1098
+ minLevel: 0,
1099
+ ...this.config.logger
1100
+ });
1101
+ logger.info("Initializing LDAP-REST client", {
1102
+ baseUrl: this.config.baseUrl,
1103
+ authType: this.config.auth.type
1104
+ });
1105
+ const auth = this.config.auth.type === "hmac" ? new HmacAuth(this.config.auth) : void 0;
1106
+ const http = new HttpClient(ConfigValidator.toHttpConfig(this.config), auth, logger);
1107
+ this.users = new UsersResource(http);
1108
+ this.organizations = new OrganizationsResource(http);
1109
+ this.groups = new GroupsResource(http);
1110
+ }
1111
+ /**
1112
+ * Gets the configured base URL
1113
+ *
1114
+ * @returns {string} The base URL of the LDAP-REST API
1115
+ */
1116
+ getBaseUrl = () => {
1117
+ return this.config.baseUrl;
1118
+ };
1119
+ };
1120
+ export {
1121
+ ApiError,
1122
+ AuthenticationError,
1123
+ AuthorizationError,
1124
+ ConflictError,
1125
+ GroupsResource,
1126
+ LdapRestClient,
1127
+ LdapRestError,
1128
+ NetworkError,
1129
+ NotFoundError,
1130
+ OrganizationsResource,
1131
+ RateLimitError,
1132
+ UsersResource,
1133
+ ValidationError
1134
+ };