ldap-rest 0.2.2 → 0.3.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 (53) hide show
  1. package/README.md +1 -1
  2. package/dist/abstract/ldapFlat.js +1 -1
  3. package/dist/bin/index.js +1 -1
  4. package/dist/config/args.js +1 -1
  5. package/dist/lib/expressFormatedResponses.js +1 -1
  6. package/dist/plugins/auth/authzDynamic.js +1 -0
  7. package/dist/plugins/auth/authzDynamicHash.js +1 -0
  8. package/dist/plugins/ldap/organization.js +1 -1
  9. package/dist/plugins/ldap/organizations.js +1 -0
  10. package/dist/plugins/scim/baseResolver.js +1 -0
  11. package/dist/plugins/scim/bulk.js +1 -0
  12. package/dist/plugins/scim/discovery.js +1 -0
  13. package/dist/plugins/scim/errors.js +1 -0
  14. package/dist/plugins/scim/filter.js +1 -0
  15. package/dist/plugins/scim/groups.js +1 -0
  16. package/dist/plugins/scim/mapping.js +1 -0
  17. package/dist/plugins/scim/patch.js +1 -0
  18. package/dist/plugins/scim/scim.js +1 -0
  19. package/dist/plugins/scim/types.js +1 -0
  20. package/dist/plugins/scim/users.js +1 -0
  21. package/dist/src/abstract/ldapFlat.d.ts +127 -0
  22. package/dist/src/config/args.d.ts +24 -0
  23. package/dist/src/plugins/auth/authzDynamic.d.ts +117 -0
  24. package/dist/src/plugins/auth/authzDynamicHash.d.ts +27 -0
  25. package/dist/src/plugins/configApi.d.ts +51 -0
  26. package/dist/src/plugins/demo/helloworld.d.ts +21 -0
  27. package/dist/src/plugins/ldap/bulkImport.d.ts +65 -0
  28. package/dist/src/plugins/ldap/groups.d.ts +68 -0
  29. package/dist/src/plugins/ldap/organization.d.ts +8 -65
  30. package/dist/src/plugins/ldap/organizations.d.ts +155 -0
  31. package/dist/src/plugins/ldap/passwordPolicy.d.ts +178 -0
  32. package/dist/src/plugins/scim/baseResolver.d.ts +23 -0
  33. package/dist/src/plugins/scim/bulk.d.ts +43 -0
  34. package/dist/src/plugins/scim/discovery.d.ts +27 -0
  35. package/dist/src/plugins/scim/errors.d.ts +37 -0
  36. package/dist/src/plugins/scim/filter.d.ts +13 -0
  37. package/dist/src/plugins/scim/groups.d.ts +61 -0
  38. package/dist/src/plugins/scim/mapping.d.ts +52 -0
  39. package/dist/src/plugins/scim/patch.d.ts +39 -0
  40. package/dist/src/plugins/scim/scim.d.ts +412 -0
  41. package/dist/src/plugins/scim/types.d.ts +222 -0
  42. package/dist/src/plugins/scim/users.d.ts +58 -0
  43. package/dist/src/plugins/static.d.ts +33 -0
  44. package/dist/src/plugins/twake/appAccountsApi.d.ts +63 -0
  45. package/dist/src/plugins/twake/james.d.ts +47 -0
  46. package/docs/plugin-development/README.md +2 -1
  47. package/docs/plugin-development/openapi.md +144 -0
  48. package/docs/usage/plugins/auth/authz-dynamic.md +183 -0
  49. package/docs/usage/plugins/integrations/scim.md +567 -0
  50. package/package.json +72 -1
  51. package/static/schemas/scim/Group.json +31 -0
  52. package/static/schemas/scim/User.json +147 -0
  53. package/static/schemas/scim/default-mapping.json +48 -0
@@ -11,6 +11,184 @@
11
11
  */
12
12
  import type { Express } from 'express';
13
13
  import DmPlugin, { type Role } from '../../abstract/plugin';
14
+ /**
15
+ * Shared OpenAPI schemas surfaced by this plugin. Picked up by
16
+ * scripts/generate-openapi.ts and merged into `components.schemas`.
17
+ *
18
+ * @openapi-component
19
+ * PasswordPolicy:
20
+ * type: object
21
+ * description: |
22
+ * Active OpenLDAP ppolicy configuration. All duration fields are in
23
+ * **seconds**. Absent fields mean the policy attribute is not set.
24
+ * properties:
25
+ * dn:
26
+ * type: string
27
+ * description: DN of the ppolicy entry.
28
+ * example: cn=default,ou=policies,dc=example,dc=com
29
+ * pwdMaxAge:
30
+ * type: integer
31
+ * description: Maximum password age in seconds (0 = never expires).
32
+ * example: 7776000
33
+ * pwdMinAge:
34
+ * type: integer
35
+ * description: Minimum password age in seconds before it may be changed.
36
+ * example: 0
37
+ * pwdInHistory:
38
+ * type: integer
39
+ * description: Number of previous passwords stored.
40
+ * example: 5
41
+ * pwdCheckQuality:
42
+ * type: integer
43
+ * description: Password quality enforcement level (0-2).
44
+ * example: 1
45
+ * pwdMinLength:
46
+ * type: integer
47
+ * description: Minimum password length.
48
+ * example: 12
49
+ * pwdMaxFailure:
50
+ * type: integer
51
+ * description: Maximum bind failures before lockout.
52
+ * example: 5
53
+ * pwdLockout:
54
+ * type: boolean
55
+ * description: Whether account lockout is enabled.
56
+ * example: true
57
+ * pwdLockoutDuration:
58
+ * type: integer
59
+ * description: Lockout duration in seconds (0 = permanent until admin reset).
60
+ * example: 300
61
+ * pwdGraceAuthNLimit:
62
+ * type: integer
63
+ * description: Number of grace logins allowed after password expires.
64
+ * example: 0
65
+ * pwdExpireWarning:
66
+ * type: integer
67
+ * description: Seconds before expiry at which a warning is issued.
68
+ * example: 1209600
69
+ * pwdMustChange:
70
+ * type: boolean
71
+ * description: If true, users must change their password after an admin reset.
72
+ * example: true
73
+ * pwdAllowUserChange:
74
+ * type: boolean
75
+ * description: Whether users are allowed to change their own password.
76
+ * example: true
77
+ * PasswordStatus:
78
+ * type: object
79
+ * description: Current password status for a specific user account.
80
+ * required: [dn, passwordSet, isExpired, isExpiringSoon, mustChange, isLocked, failureCount, graceLoginsUsed]
81
+ * properties:
82
+ * dn:
83
+ * type: string
84
+ * description: Fully-qualified DN of the user.
85
+ * example: uid=alice,ou=users,dc=example,dc=com
86
+ * passwordSet:
87
+ * type: boolean
88
+ * description: Whether a userPassword attribute is present.
89
+ * example: true
90
+ * lastChanged:
91
+ * type: string
92
+ * format: date-time
93
+ * nullable: true
94
+ * description: ISO-8601 timestamp of last password change, or null if never set.
95
+ * example: '2026-01-15T10:00:00.000Z'
96
+ * expiresAt:
97
+ * type: string
98
+ * format: date-time
99
+ * nullable: true
100
+ * description: ISO-8601 expiry timestamp, or null if no max age is configured.
101
+ * example: '2026-04-15T10:00:00.000Z'
102
+ * daysUntilExpiration:
103
+ * type: integer
104
+ * nullable: true
105
+ * description: Remaining days until expiry (negative means already expired).
106
+ * example: 80
107
+ * isExpired:
108
+ * type: boolean
109
+ * example: false
110
+ * isExpiringSoon:
111
+ * type: boolean
112
+ * description: True when expiry is within the configured warning window.
113
+ * example: false
114
+ * mustChange:
115
+ * type: boolean
116
+ * description: True when `pwdReset` is set (admin forced change).
117
+ * example: false
118
+ * isLocked:
119
+ * type: boolean
120
+ * description: True when the account is currently locked.
121
+ * example: false
122
+ * lockedAt:
123
+ * type: string
124
+ * format: date-time
125
+ * nullable: true
126
+ * description: ISO-8601 timestamp of lockout, or null.
127
+ * example: null
128
+ * failureCount:
129
+ * type: integer
130
+ * description: Number of recorded bind failures still within the window.
131
+ * example: 0
132
+ * graceLoginsUsed:
133
+ * type: integer
134
+ * description: Number of grace logins already consumed.
135
+ * example: 0
136
+ * ExpiringAccount:
137
+ * type: object
138
+ * description: User whose password is expiring within the warning window.
139
+ * required: [dn, expiresAt, daysUntilExpiration]
140
+ * properties:
141
+ * dn: { type: string, example: uid=bob,ou=users,dc=example,dc=com }
142
+ * uid: { type: string, example: bob }
143
+ * displayName: { type: string, example: Bob Builder }
144
+ * mail: { type: string, example: bob@example.com }
145
+ * expiresAt:
146
+ * type: string
147
+ * format: date-time
148
+ * example: '2026-05-01T08:00:00.000Z'
149
+ * daysUntilExpiration: { type: integer, example: 6 }
150
+ * LockedAccount:
151
+ * type: object
152
+ * description: User account that is currently locked.
153
+ * required: [dn, failureCount]
154
+ * properties:
155
+ * dn: { type: string, example: uid=charlie,ou=users,dc=example,dc=com }
156
+ * uid: { type: string, example: charlie }
157
+ * displayName: { type: string, example: Charlie Chaplin }
158
+ * mail: { type: string, example: charlie@example.com }
159
+ * lockedAt:
160
+ * type: string
161
+ * format: date-time
162
+ * nullable: true
163
+ * example: '2026-04-24T14:30:00.000Z'
164
+ * failureCount: { type: integer, example: 5 }
165
+ * PasswordValidationRequest:
166
+ * type: object
167
+ * required: [password]
168
+ * properties:
169
+ * password:
170
+ * type: string
171
+ * format: password
172
+ * description: The candidate password to validate.
173
+ * example: 'S3cur3P@ss!'
174
+ * dn:
175
+ * type: string
176
+ * description: Optional user DN for context (not used in local validation).
177
+ * example: uid=alice,ou=users,dc=example,dc=com
178
+ * PasswordValidationResponse:
179
+ * type: object
180
+ * required: [valid, errors]
181
+ * properties:
182
+ * valid:
183
+ * type: boolean
184
+ * description: True when the password meets all complexity rules.
185
+ * example: true
186
+ * errors:
187
+ * type: array
188
+ * items: { type: string }
189
+ * description: List of failed rule descriptions (empty when valid).
190
+ * example: []
191
+ */
14
192
  export default class PasswordPolicy extends DmPlugin {
15
193
  name: string;
16
194
  roles: Role[];
@@ -0,0 +1,23 @@
1
+ import type { Config } from '../../config/args';
2
+ import type { DmRequest } from '../../lib/auth/base';
3
+ export interface BaseMapEntry {
4
+ userBase?: string;
5
+ groupBase?: string;
6
+ }
7
+ export type BaseMap = Record<string, BaseMapEntry>;
8
+ export declare class BaseResolver {
9
+ private readonly defaultUserBase;
10
+ private readonly defaultGroupBase;
11
+ private readonly userTemplate;
12
+ private readonly groupTemplate;
13
+ private readonly map;
14
+ constructor(config: Config);
15
+ private applyTemplate;
16
+ private resolve;
17
+ userBase(req?: DmRequest | {
18
+ user?: string;
19
+ }): string;
20
+ groupBase(req?: DmRequest | {
21
+ user?: string;
22
+ }): string;
23
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @module plugins/scim/bulk
3
+ * @author Xavier Guimard <xguimard@linagora.com>
4
+ *
5
+ * SCIM 2.0 Bulk operations (RFC 7644 §3.7).
6
+ *
7
+ * Executes operations sequentially. Supports `bulkId` cross-references:
8
+ * a POST with `"bulkId":"abc"` creates a resource; subsequent operations
9
+ * in the same request can reference it as "bulkId:abc" anywhere a SCIM
10
+ * id or member.value is expected (resolved to the concrete id before
11
+ * dispatch).
12
+ */
13
+ import type winston from 'winston';
14
+ import type { Config } from '../../config/args';
15
+ import type { DmRequest } from '../../lib/auth/base';
16
+ import type { ScimUsers } from './users';
17
+ import type { ScimGroups } from './groups';
18
+ import { type BulkRequest, type BulkResponse } from './types';
19
+ import { writeScimErrorFromException } from './errors';
20
+ export interface ScimBulkOptions {
21
+ config: Config;
22
+ logger: winston.Logger;
23
+ users: ScimUsers;
24
+ groups: ScimGroups;
25
+ scimPrefix: string;
26
+ hooks: {
27
+ [K: string]: Function[] | undefined;
28
+ };
29
+ }
30
+ export declare class ScimBulk {
31
+ private readonly config;
32
+ private readonly logger;
33
+ private readonly users;
34
+ private readonly groups;
35
+ private readonly scimPrefix;
36
+ private readonly hooks;
37
+ constructor(opts: ScimBulkOptions);
38
+ execute(req: DmRequest, body: BulkRequest): Promise<BulkResponse>;
39
+ private executeOne;
40
+ private resolveRefs;
41
+ private buildLocation;
42
+ }
43
+ export { writeScimErrorFromException };
@@ -0,0 +1,27 @@
1
+ import type { Config } from '../../config/args';
2
+ import type { DmRequest } from '../../lib/auth/base';
3
+ import { type ResourceTypeDefinition, type ServiceProviderConfig, type SchemaDefinition } from './types';
4
+ export interface DiscoveryOptions {
5
+ config: Config;
6
+ schemaDir: string;
7
+ scimPrefix: string;
8
+ loadedPlugins: {
9
+ [name: string]: unknown;
10
+ };
11
+ }
12
+ export declare class ScimDiscovery {
13
+ private readonly config;
14
+ private readonly schemaDir;
15
+ private readonly scimPrefix;
16
+ private readonly loadedPlugins;
17
+ constructor(opts: DiscoveryOptions);
18
+ private baseUrl;
19
+ private location;
20
+ private authSchemes;
21
+ serviceProviderConfig(req?: DmRequest): ServiceProviderConfig;
22
+ resourceTypes(req?: DmRequest): ResourceTypeDefinition[];
23
+ resourceType(name: string, req?: DmRequest): ResourceTypeDefinition | undefined;
24
+ schemas(req?: DmRequest): SchemaDefinition[];
25
+ schema(id: string, req?: DmRequest): SchemaDefinition | undefined;
26
+ private loadSchema;
27
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @module plugins/scim/errors
3
+ * @author Xavier Guimard <xguimard@linagora.com>
4
+ *
5
+ * SCIM 2.0 error envelope (RFC 7644 §3.12) and async handler wrapper.
6
+ */
7
+ import type { Request, Response, NextFunction, RequestHandler } from 'express';
8
+ import { HttpError } from '../../lib/errors';
9
+ import { type ScimErrorType } from './types';
10
+ export declare const SCIM_CONTENT_TYPE = "application/scim+json";
11
+ export declare class ScimError extends HttpError {
12
+ scimType?: ScimErrorType;
13
+ constructor(status: number, detail: string, scimType?: ScimErrorType);
14
+ }
15
+ export declare const scimInvalidFilter: (detail?: string) => ScimError;
16
+ export declare const scimInvalidPath: (detail?: string) => ScimError;
17
+ export declare const scimInvalidValue: (detail?: string) => ScimError;
18
+ export declare const scimInvalidSyntax: (detail?: string) => ScimError;
19
+ export declare const scimNoTarget: (detail?: string) => ScimError;
20
+ export declare const scimMutability: (detail?: string) => ScimError;
21
+ export declare const scimUniqueness: (detail?: string) => ScimError;
22
+ export declare const scimTooMany: (detail?: string) => ScimError;
23
+ export declare const scimNotFound: (detail?: string) => ScimError;
24
+ export declare function writeScimError(res: Response, status: number, detail: string, scimType?: ScimErrorType): void;
25
+ /**
26
+ * Extract an LDAP numeric error code from either a thrown ldapts error
27
+ * (has `.code`) or an Error wrapped by ldapActions (message contains the
28
+ * original text). Returns undefined if no code is detectable.
29
+ */
30
+ export declare function extractLdapCode(err: unknown): number | undefined;
31
+ export declare function writeScimErrorFromException(res: Response, err: unknown, fallbackStatus?: number): void;
32
+ /**
33
+ * Express wrapper that catches errors, serializes them as SCIM
34
+ * envelope and short-circuits the default Express error middleware.
35
+ * Accepts both sync and async handlers.
36
+ */
37
+ export declare const scimAsyncHandler: (fn: (req: Request, res: Response, next: NextFunction) => void | Promise<void>) => RequestHandler;
@@ -0,0 +1,13 @@
1
+ import type { ResourceMapping } from './types';
2
+ export interface TranslatedFilter {
3
+ ldapFilter: string;
4
+ /** True if the filter contains `id eq "..."`, which the caller should translate into a DN lookup. */
5
+ touchesId: boolean;
6
+ /** Extracted id value if the filter is a simple `id eq "..."` (single clause). */
7
+ idEquals?: string;
8
+ }
9
+ /**
10
+ * Translate a SCIM filter string into an LDAP filter.
11
+ * Returns metadata enabling the caller to short-circuit id-equals queries.
12
+ */
13
+ export declare function scimFilterToLdap(filter: string, mapping: ResourceMapping): TranslatedFilter;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @module plugins/scim/groups
3
+ * @author Xavier Guimard <xguimard@linagora.com>
4
+ *
5
+ * SCIM Groups resource handler — direct access via ldapActions.
6
+ */
7
+ import type winston from 'winston';
8
+ import type ldapActions from '../../lib/ldapActions';
9
+ import type { Config } from '../../config/args';
10
+ import type { DmRequest } from '../../lib/auth/base';
11
+ import { BaseResolver } from './baseResolver';
12
+ import { type ResourceMapping, type ScimGroup, type ListResponse, type PatchRequest } from './types';
13
+ import type { ScimUsers } from './users';
14
+ export interface GroupListQuery {
15
+ filter?: string;
16
+ startIndex?: number;
17
+ count?: number;
18
+ sortBy?: string;
19
+ sortOrder?: 'ascending' | 'descending';
20
+ }
21
+ export interface ScimGroupsOptions {
22
+ ldap: ldapActions;
23
+ config: Config;
24
+ logger: winston.Logger;
25
+ baseResolver: BaseResolver;
26
+ users: ScimUsers;
27
+ hooks: {
28
+ [K: string]: Function[] | undefined;
29
+ };
30
+ }
31
+ export declare class ScimGroups {
32
+ private readonly ldap;
33
+ private readonly config;
34
+ private readonly logger;
35
+ private readonly baseResolver;
36
+ private readonly users;
37
+ private readonly hooks;
38
+ private readonly mapping;
39
+ private readonly rdnAttribute;
40
+ private readonly objectClass;
41
+ private readonly idAttribute;
42
+ private readonly maxResults;
43
+ private readonly scimPrefix;
44
+ constructor(opts: ScimGroupsOptions);
45
+ private ctx;
46
+ private dnForId;
47
+ /**
48
+ * Resolve an LDAP member DN back to a SCIM reference (for GET responses).
49
+ * Best-effort: extract the RDN value and a guessed type (User if in user base).
50
+ */
51
+ private buildMemberResolver;
52
+ get(req: DmRequest, id: string): Promise<ScimGroup>;
53
+ list(req: DmRequest, query: GroupListQuery): Promise<ListResponse<ScimGroup>>;
54
+ create(req: DmRequest, resource: ScimGroup): Promise<ScimGroup>;
55
+ replace(req: DmRequest, id: string, resource: ScimGroup): Promise<ScimGroup>;
56
+ patch(req: DmRequest, id: string, patch: PatchRequest): Promise<ScimGroup>;
57
+ delete(req: DmRequest, id: string): Promise<void>;
58
+ /** Used internally and by Bulk to resolve a Group SCIM reference to a DN. */
59
+ resolveRef(req: DmRequest, value: string): Promise<string | undefined>;
60
+ get groupMapping(): ResourceMapping;
61
+ }
@@ -0,0 +1,52 @@
1
+ import type { AttributesList } from '../../lib/ldapActions';
2
+ import { type ScimUser, type ScimGroup, type ResourceMapping, type MultiValued } from './types';
3
+ export declare const DEFAULT_USER_MAPPING: ResourceMapping;
4
+ export declare const DEFAULT_GROUP_MAPPING: ResourceMapping;
5
+ export declare function loadMappingFile(path: string): ResourceMapping | undefined;
6
+ /**
7
+ * Build the mapping used at runtime by merging a user-supplied JSON
8
+ * override on top of the default. Override entries replace default
9
+ * entries with the same `scim` key; new entries are appended.
10
+ */
11
+ export declare function mergeMapping(base: ResourceMapping, override?: ResourceMapping): ResourceMapping;
12
+ export interface MappingContext {
13
+ /** 'rdn' (default), 'entryUUID', or any LDAP attribute name */
14
+ idAttribute: string;
15
+ rdnAttribute: string;
16
+ resourceType: 'User' | 'Group';
17
+ baseUrl?: string;
18
+ scimPrefix: string;
19
+ }
20
+ /** Resolve the SCIM id for an LDAP entry, per configuration. */
21
+ export declare function resolveScimId(entry: AttributesList, ctx: MappingContext): string | undefined;
22
+ export declare function buildLocation(id: string, ctx: MappingContext): string | undefined;
23
+ export declare function ldapToScimUser(entry: AttributesList, mapping: ResourceMapping, ctx: MappingContext): ScimUser;
24
+ /**
25
+ * Convert SCIM User body → LDAP attributes list.
26
+ * The RDN attribute is set from `userName` (or explicit `id`).
27
+ * Returns { rdn, attributes } so the caller can build the DN.
28
+ */
29
+ export declare function scimUserToLdap(user: ScimUser, mapping: ResourceMapping, ctx: MappingContext, objectClass: string[]): {
30
+ rdn: string;
31
+ attributes: AttributesList;
32
+ };
33
+ export declare function ldapToScimGroup(entry: AttributesList, mapping: ResourceMapping, ctx: MappingContext, memberResolver?: (dn: string) => MultiValued | undefined): ScimGroup;
34
+ /**
35
+ * Convert SCIM Group → LDAP attributes (members excluded; they're
36
+ * resolved separately to DNs by the handler).
37
+ */
38
+ export declare function scimGroupToLdap(group: ScimGroup, mapping: ResourceMapping, objectClass: string[]): {
39
+ rdn: string;
40
+ attributes: AttributesList;
41
+ };
42
+ /**
43
+ * List of LDAP attributes to request from the directory so all
44
+ * mapped SCIM attributes can be populated.
45
+ */
46
+ export declare function requiredLdapAttributes(mapping: ResourceMapping): string[];
47
+ /**
48
+ * Given a SCIM attribute path like "emails.value" or "name.familyName",
49
+ * return the corresponding LDAP attribute name (used by filter parser).
50
+ * Returns undefined if the path is not mapped.
51
+ */
52
+ export declare function scimPathToLdapAttribute(path: string, mapping: ResourceMapping): string | undefined;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @module plugins/scim/patch
3
+ * @author Xavier Guimard <xguimard@linagora.com>
4
+ *
5
+ * SCIM PatchOp applicator (RFC 7644 §3.5.2) → ldapts ModifyRequest.
6
+ *
7
+ * Supports:
8
+ * - op: add / remove / replace
9
+ * - simple paths: "displayName", "userName"
10
+ * - sub-attribute paths: "name.familyName"
11
+ * - multi-valued paths: "emails"
12
+ * - Group member ops via filtered path: `members[value eq "alice"]`
13
+ * - implicit path (op.value is an object)
14
+ *
15
+ * Complex filtered paths on multi-valued attributes OTHER than `members`
16
+ * (e.g. `emails[type eq "work"]`) are rejected with `invalidPath`: the
17
+ * plugin only knows how to map them for member-type resolution. Filtered
18
+ * member operations for Groups are handled via the caller-provided
19
+ * `resolveMemberRef` hook (SCIM id → LDAP DN lookup).
20
+ */
21
+ import type { ModifyRequest } from '../../lib/ldapActions';
22
+ import { type ResourceMapping, type PatchRequest } from './types';
23
+ export interface PatchContext {
24
+ mapping: ResourceMapping;
25
+ /** For Groups: resolve SCIM member value (id or $ref) → LDAP DN. Async. */
26
+ resolveMemberRef?: (value: string) => Promise<string | undefined>;
27
+ /** The LDAP attribute holding members, default 'member'. */
28
+ memberAttribute?: string;
29
+ }
30
+ export declare function patchToModifyRequest(patch: PatchRequest, ctx: PatchContext): Promise<ModifyRequest>;
31
+ /**
32
+ * Apply PATCH to a SCIM resource object in-memory (used for PUT-equivalent
33
+ * or when the resource handler prefers object-level manipulation before
34
+ * falling back to full replace).
35
+ *
36
+ * Returns the mutated resource. Used by tests; the main production path
37
+ * is patchToModifyRequest() → ldap.modify().
38
+ */
39
+ export declare function applyPatchToResource<T extends Record<string, unknown>>(resource: T, patch: PatchRequest): T;