@happyvertical/directory 0.74.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1942 @@
1
+ import { IAMClient, ListUsersCommand, CreateUserCommand, GetUserCommand, UpdateUserCommand, DeleteUserCommand, CreateGroupCommand, GetGroupCommand, DeleteGroupCommand, ListGroupsCommand, AddUserToGroupCommand, RemoveUserFromGroupCommand, ListGroupsForUserCommand, AttachUserPolicyCommand, DetachUserPolicyCommand, CreateAccessKeyCommand, DeleteAccessKeyCommand } from "@aws-sdk/client-iam";
2
+ import { OrganizationsClient, CreateOrganizationalUnitCommand, DescribeOrganizationalUnitCommand, ListOrganizationalUnitsForParentCommand, CreateAccountCommand, DescribeCreateAccountStatusCommand, ListAccountsCommand, MoveAccountCommand } from "@aws-sdk/client-organizations";
3
+ import pg from "pg";
4
+ class DirectoryError extends Error {
5
+ code;
6
+ provider;
7
+ cause;
8
+ constructor(message, code, provider, cause) {
9
+ super(message);
10
+ this.name = "DirectoryError";
11
+ this.code = code;
12
+ this.provider = provider;
13
+ this.cause = cause;
14
+ }
15
+ }
16
+ class ConnectionError extends DirectoryError {
17
+ constructor(message, provider, cause) {
18
+ super(message, "CONNECTION_ERROR", provider, cause);
19
+ this.name = "ConnectionError";
20
+ }
21
+ }
22
+ class AuthenticationError extends DirectoryError {
23
+ constructor(message, provider, cause) {
24
+ super(message, "AUTHENTICATION_ERROR", provider, cause);
25
+ this.name = "AuthenticationError";
26
+ }
27
+ }
28
+ class NotFoundError extends DirectoryError {
29
+ resourceType;
30
+ resourceId;
31
+ constructor(resourceType, resourceId, provider, cause) {
32
+ super(
33
+ `${resourceType} not found: ${resourceId}`,
34
+ "NOT_FOUND",
35
+ provider,
36
+ cause
37
+ );
38
+ this.name = "NotFoundError";
39
+ this.resourceType = resourceType;
40
+ this.resourceId = resourceId;
41
+ }
42
+ }
43
+ class ConflictError extends DirectoryError {
44
+ resourceType;
45
+ resourceId;
46
+ constructor(resourceType, resourceId, provider, cause) {
47
+ super(
48
+ `${resourceType} already exists: ${resourceId}`,
49
+ "CONFLICT",
50
+ provider,
51
+ cause
52
+ );
53
+ this.name = "ConflictError";
54
+ this.resourceType = resourceType;
55
+ this.resourceId = resourceId;
56
+ }
57
+ }
58
+ class ValidationError extends DirectoryError {
59
+ constructor(message, provider, cause) {
60
+ super(message, "VALIDATION_ERROR", provider, cause);
61
+ this.name = "ValidationError";
62
+ }
63
+ }
64
+ class RateLimitError extends DirectoryError {
65
+ retryAfter;
66
+ constructor(provider, retryAfter) {
67
+ super(
68
+ `Rate limit exceeded${retryAfter ? ` (retry after ${retryAfter}s)` : ""}`,
69
+ "RATE_LIMIT",
70
+ provider
71
+ );
72
+ this.name = "RateLimitError";
73
+ this.retryAfter = retryAfter;
74
+ }
75
+ }
76
+ function handleAwsError(error, context) {
77
+ if (error instanceof AuthenticationError || error instanceof ConnectionError || error instanceof NotFoundError || error instanceof ConflictError || error instanceof DirectoryError) {
78
+ throw error;
79
+ }
80
+ const awsError = error;
81
+ const name = awsError.name ?? "";
82
+ const message = awsError.message ?? String(error);
83
+ switch (name) {
84
+ case "EntityAlreadyExistsException":
85
+ case "DuplicateOrganizationalUnitException":
86
+ throw new ConflictError("resource", context, "aws", error);
87
+ case "NoSuchEntityException":
88
+ case "OrganizationalUnitNotFoundException":
89
+ case "AccountNotFoundException":
90
+ throw new NotFoundError("resource", context, "aws", error);
91
+ case "AccessDeniedException":
92
+ case "InvalidClientTokenId":
93
+ case "UnrecognizedClientException":
94
+ case "InvalidAccessKeyId":
95
+ throw new AuthenticationError(`${context}: ${message}`, "aws", error);
96
+ default:
97
+ throw new DirectoryError(
98
+ `${context}: ${message}`,
99
+ "AWS_ERROR",
100
+ "aws",
101
+ error
102
+ );
103
+ }
104
+ }
105
+ function mapIamUserToDirectoryUser(user) {
106
+ const displayNameTag = user.Tags?.find((t) => t.Key === "DisplayName");
107
+ const emailTag = user.Tags?.find((t) => t.Key === "Email");
108
+ return {
109
+ id: user.UserName ?? "",
110
+ username: user.UserName ?? "",
111
+ displayName: displayNameTag?.Value,
112
+ email: emailTag?.Value,
113
+ active: true,
114
+ metadata: {
115
+ arn: user.Arn,
116
+ userId: user.UserId,
117
+ createDate: user.CreateDate?.toISOString()
118
+ }
119
+ };
120
+ }
121
+ function mapIamGroupToDirectoryGroup(group) {
122
+ return {
123
+ id: group.GroupName ?? "",
124
+ name: group.GroupName ?? "",
125
+ metadata: {
126
+ arn: group.Arn,
127
+ groupId: group.GroupId,
128
+ createDate: group.CreateDate?.toISOString()
129
+ }
130
+ };
131
+ }
132
+ function mapIamUserToAwsIamUser(user) {
133
+ return {
134
+ username: user.UserName ?? "",
135
+ arn: user.Arn,
136
+ userId: user.UserId,
137
+ createDate: user.CreateDate
138
+ };
139
+ }
140
+ class AwsAdapter {
141
+ constructor(options) {
142
+ this.options = options;
143
+ const clientConfig = {
144
+ region: options.region,
145
+ ...options.credentials ? { credentials: options.credentials } : {}
146
+ };
147
+ this.orgs = new OrganizationsClient(clientConfig);
148
+ this.iam = new IAMClient(clientConfig);
149
+ }
150
+ orgs;
151
+ iam;
152
+ // ==========================================================================
153
+ // Connection
154
+ // ==========================================================================
155
+ async testConnection() {
156
+ try {
157
+ await this.iam.send(new ListUsersCommand({ MaxItems: 1 }));
158
+ return true;
159
+ } catch {
160
+ return false;
161
+ }
162
+ }
163
+ async disconnect() {
164
+ }
165
+ // ==========================================================================
166
+ // User CRUD (DirectoryAdapter -> IAM Users)
167
+ // ==========================================================================
168
+ async createUser(input) {
169
+ try {
170
+ const tags = [];
171
+ if (input.displayName) {
172
+ tags.push({ Key: "DisplayName", Value: input.displayName });
173
+ }
174
+ if (input.email) {
175
+ tags.push({ Key: "Email", Value: input.email });
176
+ }
177
+ const result = await this.iam.send(
178
+ new CreateUserCommand({
179
+ UserName: input.username,
180
+ ...tags.length > 0 ? { Tags: tags } : {}
181
+ })
182
+ );
183
+ return mapIamUserToDirectoryUser({
184
+ ...result.User,
185
+ Tags: tags
186
+ });
187
+ } catch (error) {
188
+ handleAwsError(error, `createUser(${input.username})`);
189
+ }
190
+ }
191
+ async getUser(id) {
192
+ try {
193
+ const result = await this.iam.send(new GetUserCommand({ UserName: id }));
194
+ return mapIamUserToDirectoryUser(result.User ?? {});
195
+ } catch (error) {
196
+ handleAwsError(error, `getUser(${id})`);
197
+ }
198
+ }
199
+ async updateUser(id, _input) {
200
+ try {
201
+ await this.iam.send(new UpdateUserCommand({ UserName: id }));
202
+ return this.getUser(id);
203
+ } catch (error) {
204
+ handleAwsError(error, `updateUser(${id})`);
205
+ }
206
+ }
207
+ async deleteUser(id) {
208
+ try {
209
+ await this.iam.send(new DeleteUserCommand({ UserName: id }));
210
+ } catch (error) {
211
+ handleAwsError(error, `deleteUser(${id})`);
212
+ }
213
+ }
214
+ async listUsers() {
215
+ try {
216
+ const result = await this.iam.send(new ListUsersCommand({}));
217
+ return (result.Users ?? []).map((u) => mapIamUserToDirectoryUser(u));
218
+ } catch (error) {
219
+ handleAwsError(error, "listUsers");
220
+ }
221
+ }
222
+ // ==========================================================================
223
+ // Group CRUD (DirectoryAdapter -> IAM Groups)
224
+ // ==========================================================================
225
+ async createGroup(input) {
226
+ try {
227
+ const result = await this.iam.send(
228
+ new CreateGroupCommand({ GroupName: input.name })
229
+ );
230
+ const group = mapIamGroupToDirectoryGroup(result.Group ?? {});
231
+ if (input.members) {
232
+ for (const memberId of input.members) {
233
+ await this.addUserToGroup(memberId, input.name);
234
+ }
235
+ }
236
+ return group;
237
+ } catch (error) {
238
+ handleAwsError(error, `createGroup(${input.name})`);
239
+ }
240
+ }
241
+ async getGroup(id) {
242
+ try {
243
+ const result = await this.iam.send(
244
+ new GetGroupCommand({ GroupName: id })
245
+ );
246
+ const group = mapIamGroupToDirectoryGroup(result.Group ?? {});
247
+ group.members = (result.Users ?? []).map((u) => u.UserName ?? "");
248
+ return group;
249
+ } catch (error) {
250
+ handleAwsError(error, `getGroup(${id})`);
251
+ }
252
+ }
253
+ async updateGroup(id, _input) {
254
+ return this.getGroup(id);
255
+ }
256
+ async deleteGroup(id) {
257
+ try {
258
+ await this.iam.send(new DeleteGroupCommand({ GroupName: id }));
259
+ } catch (error) {
260
+ handleAwsError(error, `deleteGroup(${id})`);
261
+ }
262
+ }
263
+ async listGroups() {
264
+ try {
265
+ const result = await this.iam.send(new ListGroupsCommand({}));
266
+ return (result.Groups ?? []).map((g) => mapIamGroupToDirectoryGroup(g));
267
+ } catch (error) {
268
+ handleAwsError(error, "listGroups");
269
+ }
270
+ }
271
+ // ==========================================================================
272
+ // Membership
273
+ // ==========================================================================
274
+ async addUserToGroup(userId, groupId) {
275
+ try {
276
+ await this.iam.send(
277
+ new AddUserToGroupCommand({
278
+ UserName: userId,
279
+ GroupName: groupId
280
+ })
281
+ );
282
+ } catch (error) {
283
+ handleAwsError(error, `addUserToGroup(${userId}, ${groupId})`);
284
+ }
285
+ }
286
+ async removeUserFromGroup(userId, groupId) {
287
+ try {
288
+ await this.iam.send(
289
+ new RemoveUserFromGroupCommand({
290
+ UserName: userId,
291
+ GroupName: groupId
292
+ })
293
+ );
294
+ } catch (error) {
295
+ handleAwsError(error, `removeUserFromGroup(${userId}, ${groupId})`);
296
+ }
297
+ }
298
+ async getGroupMembers(groupId) {
299
+ try {
300
+ const result = await this.iam.send(
301
+ new GetGroupCommand({ GroupName: groupId })
302
+ );
303
+ return (result.Users ?? []).map((u) => mapIamUserToDirectoryUser(u));
304
+ } catch (error) {
305
+ handleAwsError(error, `getGroupMembers(${groupId})`);
306
+ }
307
+ }
308
+ async getUserGroups(userId) {
309
+ try {
310
+ const result = await this.iam.send(
311
+ new ListGroupsForUserCommand({ UserName: userId })
312
+ );
313
+ return (result.Groups ?? []).map((g) => mapIamGroupToDirectoryGroup(g));
314
+ } catch (error) {
315
+ handleAwsError(error, `getUserGroups(${userId})`);
316
+ }
317
+ }
318
+ // ==========================================================================
319
+ // Organizational Units (AwsDirectoryAdapter)
320
+ // ==========================================================================
321
+ async createOrganizationalUnit(input) {
322
+ try {
323
+ const result = await this.orgs.send(
324
+ new CreateOrganizationalUnitCommand({
325
+ ParentId: input.parentId,
326
+ Name: input.name
327
+ })
328
+ );
329
+ const ou = result.OrganizationalUnit;
330
+ return {
331
+ id: ou?.Id ?? "",
332
+ name: ou?.Name ?? "",
333
+ arn: ou?.Arn,
334
+ parentId: input.parentId
335
+ };
336
+ } catch (error) {
337
+ handleAwsError(error, `createOrganizationalUnit(${input.name})`);
338
+ }
339
+ }
340
+ async getOrganizationalUnit(id) {
341
+ try {
342
+ const result = await this.orgs.send(
343
+ new DescribeOrganizationalUnitCommand({
344
+ OrganizationalUnitId: id
345
+ })
346
+ );
347
+ const ou = result.OrganizationalUnit;
348
+ return {
349
+ id: ou?.Id ?? "",
350
+ name: ou?.Name ?? "",
351
+ arn: ou?.Arn
352
+ };
353
+ } catch (error) {
354
+ handleAwsError(error, `getOrganizationalUnit(${id})`);
355
+ }
356
+ }
357
+ async listOrganizationalUnits(parentId) {
358
+ try {
359
+ const result = await this.orgs.send(
360
+ new ListOrganizationalUnitsForParentCommand({
361
+ ParentId: parentId
362
+ })
363
+ );
364
+ return (result.OrganizationalUnits ?? []).map((ou) => ({
365
+ id: ou.Id ?? "",
366
+ name: ou.Name ?? "",
367
+ arn: ou.Arn,
368
+ parentId
369
+ }));
370
+ } catch (error) {
371
+ handleAwsError(error, `listOrganizationalUnits(${parentId})`);
372
+ }
373
+ }
374
+ // ==========================================================================
375
+ // Accounts (AwsDirectoryAdapter)
376
+ // ==========================================================================
377
+ async createAccount(input) {
378
+ try {
379
+ const result = await this.orgs.send(
380
+ new CreateAccountCommand({
381
+ AccountName: input.name,
382
+ Email: input.email,
383
+ ...input.roleName ? { RoleName: input.roleName } : {}
384
+ })
385
+ );
386
+ const status = result.CreateAccountStatus;
387
+ return {
388
+ id: status?.Id ?? "",
389
+ accountId: status?.AccountId,
390
+ state: status?.State ?? "IN_PROGRESS",
391
+ failureReason: status?.FailureReason ? String(status.FailureReason) : void 0
392
+ };
393
+ } catch (error) {
394
+ handleAwsError(error, `createAccount(${input.name})`);
395
+ }
396
+ }
397
+ async getAccountCreationStatus(id) {
398
+ try {
399
+ const result = await this.orgs.send(
400
+ new DescribeCreateAccountStatusCommand({
401
+ CreateAccountRequestId: id
402
+ })
403
+ );
404
+ const status = result.CreateAccountStatus;
405
+ return {
406
+ id: status?.Id ?? "",
407
+ accountId: status?.AccountId,
408
+ state: status?.State ?? "IN_PROGRESS",
409
+ failureReason: status?.FailureReason ? String(status.FailureReason) : void 0
410
+ };
411
+ } catch (error) {
412
+ handleAwsError(error, `getAccountCreationStatus(${id})`);
413
+ }
414
+ }
415
+ async listAccounts() {
416
+ try {
417
+ const result = await this.orgs.send(new ListAccountsCommand({}));
418
+ return (result.Accounts ?? []).map((a) => ({
419
+ id: a.Id ?? "",
420
+ name: a.Name ?? "",
421
+ email: a.Email ?? "",
422
+ arn: a.Arn,
423
+ status: a.Status ? String(a.Status) : void 0
424
+ }));
425
+ } catch (error) {
426
+ handleAwsError(error, "listAccounts");
427
+ }
428
+ }
429
+ async moveAccount(accountId, sourceParentId, destParentId) {
430
+ try {
431
+ await this.orgs.send(
432
+ new MoveAccountCommand({
433
+ AccountId: accountId,
434
+ SourceParentId: sourceParentId,
435
+ DestinationParentId: destParentId
436
+ })
437
+ );
438
+ } catch (error) {
439
+ handleAwsError(
440
+ error,
441
+ `moveAccount(${accountId}, ${sourceParentId} -> ${destParentId})`
442
+ );
443
+ }
444
+ }
445
+ // ==========================================================================
446
+ // IAM Users (AwsDirectoryAdapter)
447
+ // ==========================================================================
448
+ async createIamUser(input) {
449
+ try {
450
+ const result = await this.iam.send(
451
+ new CreateUserCommand({
452
+ UserName: input.username,
453
+ ...input.path ? { Path: input.path } : {}
454
+ })
455
+ );
456
+ return mapIamUserToAwsIamUser(result.User ?? {});
457
+ } catch (error) {
458
+ handleAwsError(error, `createIamUser(${input.username})`);
459
+ }
460
+ }
461
+ async getIamUser(username) {
462
+ try {
463
+ const result = await this.iam.send(
464
+ new GetUserCommand({ UserName: username })
465
+ );
466
+ return mapIamUserToAwsIamUser(result.User ?? {});
467
+ } catch (error) {
468
+ handleAwsError(error, `getIamUser(${username})`);
469
+ }
470
+ }
471
+ async deleteIamUser(username) {
472
+ try {
473
+ await this.iam.send(new DeleteUserCommand({ UserName: username }));
474
+ } catch (error) {
475
+ handleAwsError(error, `deleteIamUser(${username})`);
476
+ }
477
+ }
478
+ async listIamUsers() {
479
+ try {
480
+ const result = await this.iam.send(new ListUsersCommand({}));
481
+ return (result.Users ?? []).map((u) => mapIamUserToAwsIamUser(u));
482
+ } catch (error) {
483
+ handleAwsError(error, "listIamUsers");
484
+ }
485
+ }
486
+ // ==========================================================================
487
+ // IAM Policies (AwsDirectoryAdapter)
488
+ // ==========================================================================
489
+ async attachUserPolicy(username, policyArn) {
490
+ try {
491
+ await this.iam.send(
492
+ new AttachUserPolicyCommand({
493
+ UserName: username,
494
+ PolicyArn: policyArn
495
+ })
496
+ );
497
+ } catch (error) {
498
+ handleAwsError(error, `attachUserPolicy(${username}, ${policyArn})`);
499
+ }
500
+ }
501
+ async detachUserPolicy(username, policyArn) {
502
+ try {
503
+ await this.iam.send(
504
+ new DetachUserPolicyCommand({
505
+ UserName: username,
506
+ PolicyArn: policyArn
507
+ })
508
+ );
509
+ } catch (error) {
510
+ handleAwsError(error, `detachUserPolicy(${username}, ${policyArn})`);
511
+ }
512
+ }
513
+ // ==========================================================================
514
+ // Access Keys (AwsDirectoryAdapter)
515
+ // ==========================================================================
516
+ async createAccessKey(username) {
517
+ try {
518
+ const result = await this.iam.send(
519
+ new CreateAccessKeyCommand({ UserName: username })
520
+ );
521
+ const key = result.AccessKey;
522
+ return {
523
+ accessKeyId: key?.AccessKeyId ?? "",
524
+ secretAccessKey: key?.SecretAccessKey ?? "",
525
+ username: key?.UserName ?? username,
526
+ createDate: key?.CreateDate
527
+ };
528
+ } catch (error) {
529
+ handleAwsError(error, `createAccessKey(${username})`);
530
+ }
531
+ }
532
+ async deleteAccessKey(username, accessKeyId) {
533
+ try {
534
+ await this.iam.send(
535
+ new DeleteAccessKeyCommand({
536
+ UserName: username,
537
+ AccessKeyId: accessKeyId
538
+ })
539
+ );
540
+ } catch (error) {
541
+ handleAwsError(error, `deleteAccessKey(${username}, ${accessKeyId})`);
542
+ }
543
+ }
544
+ }
545
+ const aws = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
546
+ __proto__: null,
547
+ AwsAdapter
548
+ }, Symbol.toStringTag, { value: "Module" }));
549
+ class KanidmAdapter {
550
+ constructor(options) {
551
+ this.options = options;
552
+ if (!options.apiToken && (!options.adminUsername || !options.adminPassword)) {
553
+ throw new ValidationError(
554
+ "KanidmAdapter requires either apiToken or both adminUsername and adminPassword",
555
+ "kanidm"
556
+ );
557
+ }
558
+ this.baseUrl = options.baseUrl.replace(/\/+$/, "");
559
+ this.timeout = options.timeout ?? 3e4;
560
+ }
561
+ baseUrl;
562
+ timeout;
563
+ adminToken = null;
564
+ adminTokenExpiry = 0;
565
+ // ==========================================================================
566
+ // Authentication
567
+ // ==========================================================================
568
+ async getAdminToken() {
569
+ if (this.options.apiToken) {
570
+ return this.options.apiToken;
571
+ }
572
+ if (this.adminToken && Date.now() < this.adminTokenExpiry) {
573
+ return this.adminToken;
574
+ }
575
+ const authUrl = `${this.baseUrl}/v1/auth`;
576
+ const initResponse = await fetch(authUrl, {
577
+ method: "POST",
578
+ headers: { "Content-Type": "application/json" },
579
+ body: JSON.stringify({
580
+ step: {
581
+ init2: {
582
+ username: this.options.adminUsername,
583
+ issue: "token",
584
+ privileged: true
585
+ }
586
+ }
587
+ }),
588
+ signal: AbortSignal.timeout(this.timeout)
589
+ });
590
+ if (!initResponse.ok) {
591
+ throw new AuthenticationError(
592
+ "Failed to initialize admin authentication",
593
+ "kanidm"
594
+ );
595
+ }
596
+ const cookies = initResponse.headers.get("set-cookie");
597
+ const beginResponse = await fetch(authUrl, {
598
+ method: "POST",
599
+ headers: {
600
+ "Content-Type": "application/json",
601
+ ...cookies ? { Cookie: cookies } : {}
602
+ },
603
+ body: JSON.stringify({ step: { begin: "password" } }),
604
+ signal: AbortSignal.timeout(this.timeout)
605
+ });
606
+ if (!beginResponse.ok) {
607
+ throw new AuthenticationError(
608
+ "Failed to begin password authentication",
609
+ "kanidm"
610
+ );
611
+ }
612
+ const credResponse = await fetch(authUrl, {
613
+ method: "POST",
614
+ headers: {
615
+ "Content-Type": "application/json",
616
+ ...cookies ? { Cookie: cookies } : {}
617
+ },
618
+ body: JSON.stringify({
619
+ step: { cred: { password: this.options.adminPassword } }
620
+ }),
621
+ signal: AbortSignal.timeout(this.timeout)
622
+ });
623
+ if (!credResponse.ok) {
624
+ throw new AuthenticationError("Invalid admin credentials", "kanidm");
625
+ }
626
+ const result = await credResponse.json();
627
+ const token = result.state?.success || result.token;
628
+ if (!token) {
629
+ throw new AuthenticationError("Failed to obtain admin token", "kanidm");
630
+ }
631
+ this.adminToken = token;
632
+ this.adminTokenExpiry = Date.now() + 36e5;
633
+ return this.adminToken;
634
+ }
635
+ // ==========================================================================
636
+ // HTTP Helper
637
+ // ==========================================================================
638
+ async request(method, path, body) {
639
+ const token = await this.getAdminToken();
640
+ const url = `${this.baseUrl}${path}`;
641
+ let response;
642
+ try {
643
+ response = await fetch(url, {
644
+ method,
645
+ headers: {
646
+ "Content-Type": "application/json",
647
+ Authorization: `Bearer ${token}`
648
+ },
649
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {},
650
+ signal: AbortSignal.timeout(this.timeout)
651
+ });
652
+ } catch (error) {
653
+ if (error instanceof TypeError) {
654
+ throw new ConnectionError(
655
+ `Failed to connect to Kanidm at ${this.baseUrl}`,
656
+ "kanidm",
657
+ error
658
+ );
659
+ }
660
+ throw error;
661
+ }
662
+ if (response.ok) {
663
+ const text = await response.text();
664
+ if (!text) return void 0;
665
+ return JSON.parse(text);
666
+ }
667
+ const errorBody = await response.text().catch(() => "");
668
+ switch (response.status) {
669
+ case 401:
670
+ case 403:
671
+ this.adminToken = null;
672
+ this.adminTokenExpiry = 0;
673
+ throw new AuthenticationError(
674
+ errorBody || `Authentication failed: ${response.status}`,
675
+ "kanidm"
676
+ );
677
+ case 404:
678
+ throw new NotFoundError("resource", path, "kanidm");
679
+ case 409:
680
+ throw new ConflictError("resource", path, "kanidm");
681
+ default:
682
+ throw new DirectoryError(
683
+ errorBody || `Request failed: ${response.status} ${response.statusText}`,
684
+ "REQUEST_ERROR",
685
+ "kanidm"
686
+ );
687
+ }
688
+ }
689
+ // ==========================================================================
690
+ // Mapping Helpers
691
+ // ==========================================================================
692
+ mapPersonToUser(data) {
693
+ const attrs = data.attrs;
694
+ return {
695
+ id: this.attrFirst(attrs, "uuid") ?? this.attrFirst(attrs, "name") ?? "",
696
+ username: this.attrFirst(attrs, "name") ?? "",
697
+ displayName: this.attrFirst(attrs, "displayname"),
698
+ email: this.attrFirst(attrs, "mail"),
699
+ active: this.attrFirst(attrs, "class") ? !attrs.class?.includes("recycled") : true,
700
+ groups: attrs.memberof,
701
+ metadata: { attrs }
702
+ };
703
+ }
704
+ mapGroupToDirectoryGroup(data) {
705
+ const attrs = data.attrs;
706
+ return {
707
+ id: this.attrFirst(attrs, "uuid") ?? this.attrFirst(attrs, "name") ?? "",
708
+ name: this.attrFirst(attrs, "name") ?? "",
709
+ displayName: this.attrFirst(attrs, "displayname"),
710
+ description: this.attrFirst(attrs, "description"),
711
+ members: attrs.member,
712
+ metadata: { attrs }
713
+ };
714
+ }
715
+ mapOAuth2Client(data) {
716
+ const attrs = data.attrs;
717
+ return {
718
+ id: this.attrFirst(attrs, "uuid") ?? this.attrFirst(attrs, "oauth2_rs_name") ?? "",
719
+ name: this.attrFirst(attrs, "oauth2_rs_name") ?? "",
720
+ displayName: this.attrFirst(attrs, "displayname"),
721
+ redirectUris: attrs.oauth2_rs_origin ?? [],
722
+ scopes: attrs.oauth2_rs_scope_map ? attrs.oauth2_rs_scope_map : void 0,
723
+ metadata: { attrs }
724
+ };
725
+ }
726
+ attrFirst(attrs, key) {
727
+ const values = attrs[key];
728
+ return values?.[0];
729
+ }
730
+ // ==========================================================================
731
+ // Connection
732
+ // ==========================================================================
733
+ async testConnection() {
734
+ try {
735
+ await this.getAdminToken();
736
+ return true;
737
+ } catch {
738
+ return false;
739
+ }
740
+ }
741
+ async disconnect() {
742
+ this.adminToken = null;
743
+ this.adminTokenExpiry = 0;
744
+ }
745
+ // ==========================================================================
746
+ // User CRUD
747
+ // ==========================================================================
748
+ async createUser(input) {
749
+ const body = {
750
+ attrs: {
751
+ name: [input.username],
752
+ ...input.displayName ? { displayname: [input.displayName] } : {},
753
+ ...input.email ? { mail: [input.email] } : {},
754
+ ...input.metadata ?? {}
755
+ }
756
+ };
757
+ await this.request("POST", "/v1/person", body);
758
+ return this.getUser(input.username);
759
+ }
760
+ async getUser(id) {
761
+ const data = await this.request(
762
+ "GET",
763
+ `/v1/person/${encodeURIComponent(id)}`
764
+ );
765
+ return this.mapPersonToUser(data);
766
+ }
767
+ async updateUser(id, input) {
768
+ const attrs = {};
769
+ if (input.displayName !== void 0) {
770
+ attrs.displayname = [input.displayName];
771
+ }
772
+ if (input.email !== void 0) {
773
+ attrs.mail = [input.email];
774
+ }
775
+ await this.request("PATCH", `/v1/person/${encodeURIComponent(id)}`, {
776
+ attrs
777
+ });
778
+ return this.getUser(id);
779
+ }
780
+ async deleteUser(id) {
781
+ await this.request("DELETE", `/v1/person/${encodeURIComponent(id)}`);
782
+ }
783
+ async listUsers() {
784
+ const data = await this.request("GET", "/v1/person");
785
+ return (data ?? []).map((entry) => this.mapPersonToUser(entry));
786
+ }
787
+ // ==========================================================================
788
+ // Group CRUD
789
+ // ==========================================================================
790
+ async createGroup(input) {
791
+ const body = {
792
+ attrs: {
793
+ name: [input.name],
794
+ ...input.displayName ? { displayname: [input.displayName] } : {},
795
+ ...input.description ? { description: [input.description] } : {},
796
+ ...input.members ? { member: input.members } : {},
797
+ ...input.metadata ?? {}
798
+ }
799
+ };
800
+ await this.request("POST", "/v1/group", body);
801
+ return this.getGroup(input.name);
802
+ }
803
+ async getGroup(id) {
804
+ const data = await this.request(
805
+ "GET",
806
+ `/v1/group/${encodeURIComponent(id)}`
807
+ );
808
+ return this.mapGroupToDirectoryGroup(data);
809
+ }
810
+ async updateGroup(id, input) {
811
+ const attrs = {};
812
+ if (input.displayName !== void 0) {
813
+ attrs.displayname = [input.displayName];
814
+ }
815
+ if (input.description !== void 0) {
816
+ attrs.description = [input.description];
817
+ }
818
+ await this.request("PATCH", `/v1/group/${encodeURIComponent(id)}`, {
819
+ attrs
820
+ });
821
+ return this.getGroup(id);
822
+ }
823
+ async deleteGroup(id) {
824
+ await this.request("DELETE", `/v1/group/${encodeURIComponent(id)}`);
825
+ }
826
+ async listGroups() {
827
+ const data = await this.request("GET", "/v1/group");
828
+ return (data ?? []).map((entry) => this.mapGroupToDirectoryGroup(entry));
829
+ }
830
+ // ==========================================================================
831
+ // Membership
832
+ // ==========================================================================
833
+ async addUserToGroup(userId, groupId) {
834
+ await this.request(
835
+ "POST",
836
+ `/v1/group/${encodeURIComponent(groupId)}/_attr/member`,
837
+ [userId]
838
+ );
839
+ }
840
+ async removeUserFromGroup(userId, groupId) {
841
+ await this.request(
842
+ "DELETE",
843
+ `/v1/group/${encodeURIComponent(groupId)}/_attr/member`,
844
+ [userId]
845
+ );
846
+ }
847
+ async getGroupMembers(groupId) {
848
+ const group = await this.getGroup(groupId);
849
+ if (!group.members || group.members.length === 0) {
850
+ return [];
851
+ }
852
+ const users = [];
853
+ for (const memberId of group.members) {
854
+ try {
855
+ const user = await this.getUser(memberId);
856
+ users.push(user);
857
+ } catch (error) {
858
+ if (!(error instanceof NotFoundError)) {
859
+ throw error;
860
+ }
861
+ }
862
+ }
863
+ return users;
864
+ }
865
+ async getUserGroups(userId) {
866
+ const user = await this.getUser(userId);
867
+ if (!user.groups || user.groups.length === 0) {
868
+ return [];
869
+ }
870
+ const groups = [];
871
+ for (const groupId of user.groups) {
872
+ try {
873
+ const group = await this.getGroup(groupId);
874
+ groups.push(group);
875
+ } catch (error) {
876
+ if (!(error instanceof NotFoundError)) {
877
+ throw error;
878
+ }
879
+ }
880
+ }
881
+ return groups;
882
+ }
883
+ // ==========================================================================
884
+ // OAuth2 Client CRUD
885
+ // ==========================================================================
886
+ async createOAuth2Client(input) {
887
+ const body = {
888
+ attrs: {
889
+ oauth2_rs_name: [input.name],
890
+ ...input.displayName ? { displayname: [input.displayName] } : {},
891
+ oauth2_rs_origin: input.redirectUris,
892
+ ...input.scopes ? { oauth2_rs_scope_map: input.scopes } : {}
893
+ }
894
+ };
895
+ await this.request("POST", "/v1/oauth2", body);
896
+ return this.getOAuth2Client(input.name);
897
+ }
898
+ async getOAuth2Client(id) {
899
+ const data = await this.request(
900
+ "GET",
901
+ `/v1/oauth2/${encodeURIComponent(id)}`
902
+ );
903
+ return this.mapOAuth2Client(data);
904
+ }
905
+ async updateOAuth2Client(id, input) {
906
+ const attrs = {};
907
+ if (input.displayName !== void 0) {
908
+ attrs.displayname = [input.displayName];
909
+ }
910
+ if (input.redirectUris !== void 0) {
911
+ attrs.oauth2_rs_origin = input.redirectUris;
912
+ }
913
+ if (input.scopes !== void 0) {
914
+ attrs.oauth2_rs_scope_map = input.scopes;
915
+ }
916
+ await this.request("PATCH", `/v1/oauth2/${encodeURIComponent(id)}`, {
917
+ attrs
918
+ });
919
+ return this.getOAuth2Client(id);
920
+ }
921
+ async deleteOAuth2Client(id) {
922
+ await this.request("DELETE", `/v1/oauth2/${encodeURIComponent(id)}`);
923
+ }
924
+ async listOAuth2Clients() {
925
+ const data = await this.request("GET", "/v1/oauth2");
926
+ return (data ?? []).map((entry) => this.mapOAuth2Client(entry));
927
+ }
928
+ // ==========================================================================
929
+ // OAuth2 Secret Management
930
+ // ==========================================================================
931
+ async getOAuth2ClientSecret(id) {
932
+ const data = await this.request(
933
+ "GET",
934
+ `/v1/oauth2/${encodeURIComponent(id)}/_basic_secret`
935
+ );
936
+ return data;
937
+ }
938
+ // ==========================================================================
939
+ // Credential Management
940
+ // ==========================================================================
941
+ async createCredentialResetIntent(userId, options) {
942
+ const ttl = options?.ttl ?? 3600;
943
+ const token = await this.request(
944
+ "GET",
945
+ `/v1/person/${encodeURIComponent(userId)}/_credential/_update_intent/${ttl}`
946
+ );
947
+ return {
948
+ token,
949
+ url: `${this.baseUrl}/ui/reset?token=${encodeURIComponent(token)}`,
950
+ ttl
951
+ };
952
+ }
953
+ }
954
+ const kanidm = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
955
+ __proto__: null,
956
+ KanidmAdapter
957
+ }, Symbol.toStringTag, { value: "Module" }));
958
+ const { Client } = pg;
959
+ const PROVIDER$1 = "postgres";
960
+ const SYSTEM_ROLE_PREFIX = "pg_";
961
+ const SYSTEM_ROLES = /* @__PURE__ */ new Set(["postgres"]);
962
+ const SYSTEM_DATABASES = /* @__PURE__ */ new Set(["template0", "template1", "postgres"]);
963
+ class PostgresAdapter {
964
+ options;
965
+ client = null;
966
+ constructor(options) {
967
+ this.options = options;
968
+ }
969
+ // ==========================================================================
970
+ // Private Helpers
971
+ // ==========================================================================
972
+ /**
973
+ * Escape a SQL identifier by wrapping in double quotes and escaping
974
+ * any embedded double quotes. Used for DDL statements where
975
+ * parameterized queries are not supported.
976
+ */
977
+ escapeIdentifier(name) {
978
+ return `"${name.replace(/"/g, '""')}"`;
979
+ }
980
+ /**
981
+ * Escape a SQL string literal by wrapping in single quotes and escaping
982
+ * any embedded single quotes. Used for DDL statements (e.g., passwords)
983
+ * where parameterized queries are not supported.
984
+ */
985
+ escapeLiteral(value) {
986
+ return `'${value.replace(/'/g, "''")}'`;
987
+ }
988
+ /**
989
+ * Get or create the pg.Client instance. Connects lazily on first use.
990
+ */
991
+ async getClient() {
992
+ if (this.client) {
993
+ return this.client;
994
+ }
995
+ try {
996
+ this.client = new Client({
997
+ host: this.options.host,
998
+ port: this.options.port ?? 5432,
999
+ user: this.options.adminUser,
1000
+ password: this.options.adminPassword,
1001
+ database: this.options.database ?? "postgres",
1002
+ ssl: this.options.ssl ? typeof this.options.ssl === "object" ? this.options.ssl : { rejectUnauthorized: false } : void 0
1003
+ });
1004
+ await this.client.connect();
1005
+ return this.client;
1006
+ } catch (error) {
1007
+ this.client = null;
1008
+ throw this.mapError(error);
1009
+ }
1010
+ }
1011
+ /**
1012
+ * Check if a role name is a system role that should be excluded from lists.
1013
+ */
1014
+ isSystemRole(rolname) {
1015
+ return rolname.startsWith(SYSTEM_ROLE_PREFIX) || SYSTEM_ROLES.has(rolname);
1016
+ }
1017
+ /**
1018
+ * Map a pg_roles row to a DirectoryUser.
1019
+ */
1020
+ rowToUser(row) {
1021
+ return {
1022
+ id: row.rolname,
1023
+ username: row.rolname,
1024
+ active: true,
1025
+ metadata: {
1026
+ superuser: row.rolsuper,
1027
+ createDb: row.rolcreatedb,
1028
+ createRole: row.rolcreaterole
1029
+ }
1030
+ };
1031
+ }
1032
+ /**
1033
+ * Map a pg_roles row to a DirectoryGroup.
1034
+ */
1035
+ rowToGroup(row) {
1036
+ return {
1037
+ id: row.rolname,
1038
+ name: row.rolname,
1039
+ metadata: {
1040
+ superuser: row.rolsuper,
1041
+ createDb: row.rolcreatedb,
1042
+ createRole: row.rolcreaterole
1043
+ }
1044
+ };
1045
+ }
1046
+ /**
1047
+ * Map a pg_roles row to a PgRole.
1048
+ */
1049
+ rowToPgRole(row) {
1050
+ return {
1051
+ name: row.rolname,
1052
+ login: row.rolcanlogin,
1053
+ superuser: row.rolsuper,
1054
+ createDb: row.rolcreatedb,
1055
+ createRole: row.rolcreaterole
1056
+ };
1057
+ }
1058
+ /**
1059
+ * Map a pg_database row to a PgDatabase.
1060
+ */
1061
+ rowToDatabase(row) {
1062
+ return {
1063
+ name: row.datname,
1064
+ owner: row.owner,
1065
+ encoding: row.encoding,
1066
+ size: row.size
1067
+ };
1068
+ }
1069
+ /**
1070
+ * Map PostgreSQL errors to directory error classes.
1071
+ */
1072
+ mapError(error) {
1073
+ if (error instanceof DirectoryError) {
1074
+ return error;
1075
+ }
1076
+ const pgError = error;
1077
+ switch (pgError.code) {
1078
+ case "42710":
1079
+ return new ConflictError(
1080
+ "role",
1081
+ pgError.message ?? "unknown",
1082
+ PROVIDER$1,
1083
+ error
1084
+ );
1085
+ case "42704":
1086
+ return new NotFoundError(
1087
+ "role",
1088
+ pgError.message ?? "unknown",
1089
+ PROVIDER$1,
1090
+ error
1091
+ );
1092
+ case "28P01":
1093
+ return new AuthenticationError(
1094
+ pgError.message ?? "Invalid password",
1095
+ PROVIDER$1,
1096
+ error
1097
+ );
1098
+ }
1099
+ const message = pgError.message ?? (error instanceof Error ? error.message : String(error));
1100
+ if (message.includes("ECONNREFUSED") || message.includes("connect ETIMEDOUT") || message.includes("getaddrinfo")) {
1101
+ return new ConnectionError(
1102
+ `Failed to connect to PostgreSQL: ${message}`,
1103
+ PROVIDER$1,
1104
+ error
1105
+ );
1106
+ }
1107
+ return new DirectoryError(message, "UNKNOWN_ERROR", PROVIDER$1, error);
1108
+ }
1109
+ // ==========================================================================
1110
+ // Connection
1111
+ // ==========================================================================
1112
+ async testConnection() {
1113
+ try {
1114
+ const client = await this.getClient();
1115
+ await client.query("SELECT 1");
1116
+ return true;
1117
+ } catch {
1118
+ return false;
1119
+ }
1120
+ }
1121
+ async disconnect() {
1122
+ if (this.client) {
1123
+ await this.client.end();
1124
+ this.client = null;
1125
+ }
1126
+ }
1127
+ // ==========================================================================
1128
+ // User CRUD (PostgreSQL roles WITH LOGIN)
1129
+ // ==========================================================================
1130
+ async createUser(input) {
1131
+ const client = await this.getClient();
1132
+ try {
1133
+ let sql = `CREATE ROLE ${this.escapeIdentifier(input.username)} WITH LOGIN`;
1134
+ if (input.password) {
1135
+ sql += ` PASSWORD ${this.escapeLiteral(input.password)}`;
1136
+ }
1137
+ await client.query(sql);
1138
+ return this.getUser(input.username);
1139
+ } catch (error) {
1140
+ throw this.mapError(error);
1141
+ }
1142
+ }
1143
+ async getUser(id) {
1144
+ const client = await this.getClient();
1145
+ try {
1146
+ const result = await client.query(
1147
+ `SELECT rolname, rolsuper, rolcreatedb, rolcreaterole, rolcanlogin
1148
+ FROM pg_roles
1149
+ WHERE rolname = $1 AND rolcanlogin = true`,
1150
+ [id]
1151
+ );
1152
+ if (result.rows.length === 0) {
1153
+ throw new NotFoundError("user", id, PROVIDER$1);
1154
+ }
1155
+ return this.rowToUser(result.rows[0]);
1156
+ } catch (error) {
1157
+ throw this.mapError(error);
1158
+ }
1159
+ }
1160
+ async updateUser(id, input) {
1161
+ const client = await this.getClient();
1162
+ try {
1163
+ await this.getUser(id);
1164
+ const alterClauses = [];
1165
+ if (input.password !== void 0) {
1166
+ alterClauses.push(`PASSWORD ${this.escapeLiteral(input.password)}`);
1167
+ }
1168
+ if (alterClauses.length > 0) {
1169
+ const sql = `ALTER ROLE ${this.escapeIdentifier(id)} WITH ${alterClauses.join(" ")}`;
1170
+ await client.query(sql);
1171
+ }
1172
+ return this.getUser(id);
1173
+ } catch (error) {
1174
+ throw this.mapError(error);
1175
+ }
1176
+ }
1177
+ async deleteUser(id) {
1178
+ const client = await this.getClient();
1179
+ try {
1180
+ await this.getUser(id);
1181
+ await client.query(`DROP ROLE ${this.escapeIdentifier(id)}`);
1182
+ } catch (error) {
1183
+ throw this.mapError(error);
1184
+ }
1185
+ }
1186
+ async listUsers() {
1187
+ const client = await this.getClient();
1188
+ try {
1189
+ const result = await client.query(
1190
+ `SELECT rolname, rolsuper, rolcreatedb, rolcreaterole, rolcanlogin
1191
+ FROM pg_roles
1192
+ WHERE rolcanlogin = true
1193
+ ORDER BY rolname`
1194
+ );
1195
+ return result.rows.filter(
1196
+ (row) => !this.isSystemRole(row.rolname)
1197
+ ).map((row) => this.rowToUser(row));
1198
+ } catch (error) {
1199
+ throw this.mapError(error);
1200
+ }
1201
+ }
1202
+ // ==========================================================================
1203
+ // Group CRUD (PostgreSQL roles WITHOUT LOGIN)
1204
+ // ==========================================================================
1205
+ async createGroup(input) {
1206
+ const client = await this.getClient();
1207
+ try {
1208
+ await client.query(
1209
+ `CREATE ROLE ${this.escapeIdentifier(input.name)} NOLOGIN`
1210
+ );
1211
+ if (input.members) {
1212
+ for (const member of input.members) {
1213
+ await this.addUserToGroup(member, input.name);
1214
+ }
1215
+ }
1216
+ return this.getGroup(input.name);
1217
+ } catch (error) {
1218
+ throw this.mapError(error);
1219
+ }
1220
+ }
1221
+ async getGroup(id) {
1222
+ const client = await this.getClient();
1223
+ try {
1224
+ const result = await client.query(
1225
+ `SELECT rolname, rolsuper, rolcreatedb, rolcreaterole, rolcanlogin
1226
+ FROM pg_roles
1227
+ WHERE rolname = $1 AND rolcanlogin = false`,
1228
+ [id]
1229
+ );
1230
+ if (result.rows.length === 0) {
1231
+ throw new NotFoundError("group", id, PROVIDER$1);
1232
+ }
1233
+ return this.rowToGroup(result.rows[0]);
1234
+ } catch (error) {
1235
+ throw this.mapError(error);
1236
+ }
1237
+ }
1238
+ async updateGroup(id, _input) {
1239
+ return this.getGroup(id);
1240
+ }
1241
+ async deleteGroup(id) {
1242
+ const client = await this.getClient();
1243
+ try {
1244
+ await this.getGroup(id);
1245
+ await client.query(`DROP ROLE ${this.escapeIdentifier(id)}`);
1246
+ } catch (error) {
1247
+ throw this.mapError(error);
1248
+ }
1249
+ }
1250
+ async listGroups() {
1251
+ const client = await this.getClient();
1252
+ try {
1253
+ const result = await client.query(
1254
+ `SELECT rolname, rolsuper, rolcreatedb, rolcreaterole, rolcanlogin
1255
+ FROM pg_roles
1256
+ WHERE rolcanlogin = false
1257
+ ORDER BY rolname`
1258
+ );
1259
+ return result.rows.filter(
1260
+ (row) => !this.isSystemRole(row.rolname)
1261
+ ).map((row) => this.rowToGroup(row));
1262
+ } catch (error) {
1263
+ throw this.mapError(error);
1264
+ }
1265
+ }
1266
+ // ==========================================================================
1267
+ // Membership (GRANT/REVOKE role TO/FROM role)
1268
+ // ==========================================================================
1269
+ async addUserToGroup(userId, groupId) {
1270
+ const client = await this.getClient();
1271
+ try {
1272
+ await client.query(
1273
+ `GRANT ${this.escapeIdentifier(groupId)} TO ${this.escapeIdentifier(userId)}`
1274
+ );
1275
+ } catch (error) {
1276
+ throw this.mapError(error);
1277
+ }
1278
+ }
1279
+ async removeUserFromGroup(userId, groupId) {
1280
+ const client = await this.getClient();
1281
+ try {
1282
+ await client.query(
1283
+ `REVOKE ${this.escapeIdentifier(groupId)} FROM ${this.escapeIdentifier(userId)}`
1284
+ );
1285
+ } catch (error) {
1286
+ throw this.mapError(error);
1287
+ }
1288
+ }
1289
+ async getGroupMembers(groupId) {
1290
+ const client = await this.getClient();
1291
+ try {
1292
+ await this.getGroup(groupId);
1293
+ const result = await client.query(
1294
+ `SELECT r.rolname, r.rolsuper, r.rolcreatedb, r.rolcreaterole, r.rolcanlogin
1295
+ FROM pg_auth_members m
1296
+ JOIN pg_roles g ON g.oid = m.roleid
1297
+ JOIN pg_roles r ON r.oid = m.member
1298
+ WHERE g.rolname = $1 AND r.rolcanlogin = true
1299
+ ORDER BY r.rolname`,
1300
+ [groupId]
1301
+ );
1302
+ return result.rows.map(
1303
+ (row) => this.rowToUser(row)
1304
+ );
1305
+ } catch (error) {
1306
+ throw this.mapError(error);
1307
+ }
1308
+ }
1309
+ async getUserGroups(userId) {
1310
+ const client = await this.getClient();
1311
+ try {
1312
+ await this.getUser(userId);
1313
+ const result = await client.query(
1314
+ `SELECT g.rolname, g.rolsuper, g.rolcreatedb, g.rolcreaterole, g.rolcanlogin
1315
+ FROM pg_auth_members m
1316
+ JOIN pg_roles g ON g.oid = m.roleid
1317
+ JOIN pg_roles r ON r.oid = m.member
1318
+ WHERE r.rolname = $1 AND g.rolcanlogin = false
1319
+ ORDER BY g.rolname`,
1320
+ [userId]
1321
+ );
1322
+ return result.rows.map(
1323
+ (row) => this.rowToGroup(row)
1324
+ );
1325
+ } catch (error) {
1326
+ throw this.mapError(error);
1327
+ }
1328
+ }
1329
+ // ==========================================================================
1330
+ // Database Provisioning
1331
+ // ==========================================================================
1332
+ async createDatabase(input) {
1333
+ const client = await this.getClient();
1334
+ try {
1335
+ let sql = `CREATE DATABASE ${this.escapeIdentifier(input.name)} OWNER ${this.escapeIdentifier(input.owner)}`;
1336
+ if (input.encoding) {
1337
+ sql += ` ENCODING ${this.escapeLiteral(input.encoding)}`;
1338
+ }
1339
+ await client.query(sql);
1340
+ return this.getDatabase(input.name);
1341
+ } catch (error) {
1342
+ throw this.mapError(error);
1343
+ }
1344
+ }
1345
+ async getDatabase(name) {
1346
+ const client = await this.getClient();
1347
+ try {
1348
+ const result = await client.query(
1349
+ `SELECT
1350
+ d.datname,
1351
+ pg_catalog.pg_get_userbyid(d.datdba) AS owner,
1352
+ pg_encoding_to_char(d.encoding) AS encoding,
1353
+ pg_database_size(d.datname)::text AS size
1354
+ FROM pg_database d
1355
+ WHERE d.datname = $1`,
1356
+ [name]
1357
+ );
1358
+ if (result.rows.length === 0) {
1359
+ throw new NotFoundError("database", name, PROVIDER$1);
1360
+ }
1361
+ return this.rowToDatabase(result.rows[0]);
1362
+ } catch (error) {
1363
+ throw this.mapError(error);
1364
+ }
1365
+ }
1366
+ async dropDatabase(name) {
1367
+ const client = await this.getClient();
1368
+ try {
1369
+ await this.getDatabase(name);
1370
+ await client.query(`DROP DATABASE ${this.escapeIdentifier(name)}`);
1371
+ } catch (error) {
1372
+ throw this.mapError(error);
1373
+ }
1374
+ }
1375
+ async listDatabases() {
1376
+ const client = await this.getClient();
1377
+ try {
1378
+ const result = await client.query(
1379
+ `SELECT
1380
+ d.datname,
1381
+ pg_catalog.pg_get_userbyid(d.datdba) AS owner,
1382
+ pg_encoding_to_char(d.encoding) AS encoding,
1383
+ pg_database_size(d.datname)::text AS size
1384
+ FROM pg_database d
1385
+ ORDER BY d.datname`
1386
+ );
1387
+ return result.rows.filter(
1388
+ (row) => !SYSTEM_DATABASES.has(row.datname)
1389
+ ).map((row) => this.rowToDatabase(row));
1390
+ } catch (error) {
1391
+ throw this.mapError(error);
1392
+ }
1393
+ }
1394
+ // ==========================================================================
1395
+ // Role Provisioning
1396
+ // ==========================================================================
1397
+ async createRole(input) {
1398
+ const client = await this.getClient();
1399
+ try {
1400
+ const options = [];
1401
+ if (input.login !== void 0) {
1402
+ options.push(input.login ? "LOGIN" : "NOLOGIN");
1403
+ }
1404
+ if (input.password) {
1405
+ options.push(`PASSWORD ${this.escapeLiteral(input.password)}`);
1406
+ }
1407
+ if (input.superuser !== void 0) {
1408
+ options.push(input.superuser ? "SUPERUSER" : "NOSUPERUSER");
1409
+ }
1410
+ if (input.createDb !== void 0) {
1411
+ options.push(input.createDb ? "CREATEDB" : "NOCREATEDB");
1412
+ }
1413
+ if (input.createRole !== void 0) {
1414
+ options.push(input.createRole ? "CREATEROLE" : "NOCREATEROLE");
1415
+ }
1416
+ let sql = `CREATE ROLE ${this.escapeIdentifier(input.name)}`;
1417
+ if (options.length > 0) {
1418
+ sql += ` WITH ${options.join(" ")}`;
1419
+ }
1420
+ await client.query(sql);
1421
+ return this.getRole(input.name);
1422
+ } catch (error) {
1423
+ throw this.mapError(error);
1424
+ }
1425
+ }
1426
+ async getRole(name) {
1427
+ const client = await this.getClient();
1428
+ try {
1429
+ const result = await client.query(
1430
+ `SELECT rolname, rolcanlogin, rolsuper, rolcreatedb, rolcreaterole
1431
+ FROM pg_roles
1432
+ WHERE rolname = $1`,
1433
+ [name]
1434
+ );
1435
+ if (result.rows.length === 0) {
1436
+ throw new NotFoundError("role", name, PROVIDER$1);
1437
+ }
1438
+ return this.rowToPgRole(result.rows[0]);
1439
+ } catch (error) {
1440
+ throw this.mapError(error);
1441
+ }
1442
+ }
1443
+ async dropRole(name) {
1444
+ const client = await this.getClient();
1445
+ try {
1446
+ await this.getRole(name);
1447
+ await client.query(`DROP ROLE ${this.escapeIdentifier(name)}`);
1448
+ } catch (error) {
1449
+ throw this.mapError(error);
1450
+ }
1451
+ }
1452
+ async listRoles() {
1453
+ const client = await this.getClient();
1454
+ try {
1455
+ const result = await client.query(
1456
+ `SELECT rolname, rolcanlogin, rolsuper, rolcreatedb, rolcreaterole
1457
+ FROM pg_roles
1458
+ ORDER BY rolname`
1459
+ );
1460
+ return result.rows.filter(
1461
+ (row) => !this.isSystemRole(row.rolname)
1462
+ ).map((row) => this.rowToPgRole(row));
1463
+ } catch (error) {
1464
+ throw this.mapError(error);
1465
+ }
1466
+ }
1467
+ // ==========================================================================
1468
+ // Access Control (GRANT/REVOKE on databases)
1469
+ // ==========================================================================
1470
+ async grantAccess(roleName, databaseName) {
1471
+ const client = await this.getClient();
1472
+ try {
1473
+ await client.query(
1474
+ `GRANT ALL ON DATABASE ${this.escapeIdentifier(databaseName)} TO ${this.escapeIdentifier(roleName)}`
1475
+ );
1476
+ } catch (error) {
1477
+ throw this.mapError(error);
1478
+ }
1479
+ }
1480
+ async revokeAccess(roleName, databaseName) {
1481
+ const client = await this.getClient();
1482
+ try {
1483
+ await client.query(
1484
+ `REVOKE ALL ON DATABASE ${this.escapeIdentifier(databaseName)} FROM ${this.escapeIdentifier(roleName)}`
1485
+ );
1486
+ } catch (error) {
1487
+ throw this.mapError(error);
1488
+ }
1489
+ }
1490
+ }
1491
+ const postgres = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
1492
+ __proto__: null,
1493
+ PostgresAdapter
1494
+ }, Symbol.toStringTag, { value: "Module" }));
1495
+ const PROVIDER = "stalwart";
1496
+ const DEFAULT_TIMEOUT = 3e4;
1497
+ class StalwartAdapter {
1498
+ options;
1499
+ authHeader;
1500
+ constructor(options) {
1501
+ this.options = options;
1502
+ this.authHeader = `Basic ${btoa(`${options.username}:${options.password}`)}`;
1503
+ }
1504
+ get timeout() {
1505
+ return this.options.timeout ?? DEFAULT_TIMEOUT;
1506
+ }
1507
+ // ==========================================================================
1508
+ // HTTP Helper
1509
+ // ==========================================================================
1510
+ async request(method, path, body) {
1511
+ const url = `${this.options.baseUrl}${path}`;
1512
+ const headers = {
1513
+ Authorization: this.authHeader,
1514
+ Accept: "application/json"
1515
+ };
1516
+ if (body !== void 0) {
1517
+ headers["Content-Type"] = "application/json";
1518
+ }
1519
+ let response;
1520
+ try {
1521
+ response = await fetch(url, {
1522
+ method,
1523
+ headers,
1524
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
1525
+ signal: AbortSignal.timeout(this.timeout)
1526
+ });
1527
+ } catch (error) {
1528
+ if (error instanceof DOMException && error.name === "TimeoutError") {
1529
+ throw new ConnectionError(
1530
+ `Request timed out after ${this.timeout}ms: ${method} ${path}`,
1531
+ PROVIDER,
1532
+ error
1533
+ );
1534
+ }
1535
+ throw new ConnectionError(
1536
+ `Failed to connect to Stalwart: ${error.message}`,
1537
+ PROVIDER,
1538
+ error
1539
+ );
1540
+ }
1541
+ if (response.ok) {
1542
+ const text = await response.text();
1543
+ if (!text) return void 0;
1544
+ try {
1545
+ return JSON.parse(text);
1546
+ } catch {
1547
+ return text;
1548
+ }
1549
+ }
1550
+ const errorBody = await response.text().catch(() => "");
1551
+ switch (response.status) {
1552
+ case 401:
1553
+ case 403:
1554
+ throw new AuthenticationError(
1555
+ `Authentication failed: ${response.status} ${response.statusText}`,
1556
+ PROVIDER
1557
+ );
1558
+ case 404:
1559
+ throw new NotFoundError("resource", path, PROVIDER);
1560
+ case 409:
1561
+ throw new ConflictError("resource", path, PROVIDER);
1562
+ default:
1563
+ throw new DirectoryError(
1564
+ `Stalwart API error: ${response.status} ${response.statusText}${errorBody ? ` - ${errorBody}` : ""}`,
1565
+ "API_ERROR",
1566
+ PROVIDER
1567
+ );
1568
+ }
1569
+ }
1570
+ // ==========================================================================
1571
+ // Connection
1572
+ // ==========================================================================
1573
+ async testConnection() {
1574
+ try {
1575
+ await this.request("GET", "/api/principal?type=individual&limit=1");
1576
+ return true;
1577
+ } catch (error) {
1578
+ if (error instanceof AuthenticationError) {
1579
+ throw error;
1580
+ }
1581
+ return false;
1582
+ }
1583
+ }
1584
+ async disconnect() {
1585
+ }
1586
+ // ==========================================================================
1587
+ // User CRUD
1588
+ // ==========================================================================
1589
+ async createUser(input) {
1590
+ const principal = {
1591
+ name: input.username,
1592
+ type: "individual",
1593
+ description: input.displayName,
1594
+ emails: input.email ? [input.email] : [],
1595
+ secrets: input.password ? [input.password] : void 0
1596
+ };
1597
+ await this.request("POST", "/api/principal", principal);
1598
+ return this.getUser(input.username);
1599
+ }
1600
+ async getUser(id) {
1601
+ const principal = await this.request(
1602
+ "GET",
1603
+ `/api/principal/${encodeURIComponent(id)}`
1604
+ );
1605
+ return this.principalToUser(principal);
1606
+ }
1607
+ async updateUser(id, input) {
1608
+ const patch = {};
1609
+ if (input.displayName !== void 0) patch.description = input.displayName;
1610
+ if (input.email !== void 0) patch.emails = [input.email];
1611
+ if (input.password !== void 0) patch.secrets = [input.password];
1612
+ await this.request(
1613
+ "PATCH",
1614
+ `/api/principal/${encodeURIComponent(id)}`,
1615
+ patch
1616
+ );
1617
+ return this.getUser(id);
1618
+ }
1619
+ async deleteUser(id) {
1620
+ await this.request("DELETE", `/api/principal/${encodeURIComponent(id)}`);
1621
+ }
1622
+ async listUsers() {
1623
+ const names = await this.request(
1624
+ "GET",
1625
+ "/api/principal?type=individual"
1626
+ );
1627
+ const users = await Promise.all(
1628
+ (names ?? []).map((name) => this.getUser(name))
1629
+ );
1630
+ return users;
1631
+ }
1632
+ // ==========================================================================
1633
+ // Group CRUD
1634
+ // ==========================================================================
1635
+ async createGroup(input) {
1636
+ const principal = {
1637
+ name: input.name,
1638
+ type: "group",
1639
+ description: input.description ?? input.displayName,
1640
+ members: input.members
1641
+ };
1642
+ await this.request("POST", "/api/principal", principal);
1643
+ return this.getGroup(input.name);
1644
+ }
1645
+ async getGroup(id) {
1646
+ const principal = await this.request(
1647
+ "GET",
1648
+ `/api/principal/${encodeURIComponent(id)}`
1649
+ );
1650
+ return this.principalToGroup(principal);
1651
+ }
1652
+ async updateGroup(id, input) {
1653
+ const patch = {};
1654
+ if (input.displayName !== void 0) patch.description = input.displayName;
1655
+ if (input.description !== void 0) patch.description = input.description;
1656
+ await this.request(
1657
+ "PATCH",
1658
+ `/api/principal/${encodeURIComponent(id)}`,
1659
+ patch
1660
+ );
1661
+ return this.getGroup(id);
1662
+ }
1663
+ async deleteGroup(id) {
1664
+ await this.request("DELETE", `/api/principal/${encodeURIComponent(id)}`);
1665
+ }
1666
+ async listGroups() {
1667
+ const names = await this.request(
1668
+ "GET",
1669
+ "/api/principal?type=group"
1670
+ );
1671
+ const groups = await Promise.all(
1672
+ (names ?? []).map((name) => this.getGroup(name))
1673
+ );
1674
+ return groups;
1675
+ }
1676
+ // ==========================================================================
1677
+ // Membership
1678
+ // ==========================================================================
1679
+ async addUserToGroup(userId, groupId) {
1680
+ const group = await this.request(
1681
+ "GET",
1682
+ `/api/principal/${encodeURIComponent(groupId)}`
1683
+ );
1684
+ const members = group.members ?? [];
1685
+ if (!members.includes(userId)) {
1686
+ members.push(userId);
1687
+ }
1688
+ await this.request(
1689
+ "PATCH",
1690
+ `/api/principal/${encodeURIComponent(groupId)}`,
1691
+ { members }
1692
+ );
1693
+ }
1694
+ async removeUserFromGroup(userId, groupId) {
1695
+ const group = await this.request(
1696
+ "GET",
1697
+ `/api/principal/${encodeURIComponent(groupId)}`
1698
+ );
1699
+ const members = (group.members ?? []).filter((m) => m !== userId);
1700
+ await this.request(
1701
+ "PATCH",
1702
+ `/api/principal/${encodeURIComponent(groupId)}`,
1703
+ { members }
1704
+ );
1705
+ }
1706
+ async getGroupMembers(groupId) {
1707
+ const group = await this.request(
1708
+ "GET",
1709
+ `/api/principal/${encodeURIComponent(groupId)}`
1710
+ );
1711
+ const members = group.members ?? [];
1712
+ const users = await Promise.all(members.map((name) => this.getUser(name)));
1713
+ return users;
1714
+ }
1715
+ async getUserGroups(userId) {
1716
+ const user = await this.request(
1717
+ "GET",
1718
+ `/api/principal/${encodeURIComponent(userId)}`
1719
+ );
1720
+ const groupNames = user.memberOf ?? [];
1721
+ const groups = await Promise.all(
1722
+ groupNames.map((name) => this.getGroup(name))
1723
+ );
1724
+ return groups;
1725
+ }
1726
+ // ==========================================================================
1727
+ // Domain CRUD
1728
+ // ==========================================================================
1729
+ async createDomain(input) {
1730
+ const principal = {
1731
+ name: input.name,
1732
+ type: "domain"
1733
+ };
1734
+ await this.request("POST", "/api/principal", principal);
1735
+ return this.getDomain(input.name);
1736
+ }
1737
+ async getDomain(id) {
1738
+ const principal = await this.request(
1739
+ "GET",
1740
+ `/api/principal/${encodeURIComponent(id)}`
1741
+ );
1742
+ return {
1743
+ id: principal.name,
1744
+ name: principal.name,
1745
+ active: true
1746
+ };
1747
+ }
1748
+ async deleteDomain(id) {
1749
+ await this.request("DELETE", `/api/principal/${encodeURIComponent(id)}`);
1750
+ }
1751
+ async listDomains() {
1752
+ const names = await this.request(
1753
+ "GET",
1754
+ "/api/principal?type=domain"
1755
+ );
1756
+ const domains = await Promise.all(
1757
+ (names ?? []).map((name) => this.getDomain(name))
1758
+ );
1759
+ return domains;
1760
+ }
1761
+ // ==========================================================================
1762
+ // DKIM
1763
+ // ==========================================================================
1764
+ async createDkimKey(input) {
1765
+ const result = await this.request("POST", "/api/dkim", {
1766
+ domain: input.domain,
1767
+ selector: input.selector
1768
+ });
1769
+ return {
1770
+ id: result?.id ?? `${input.selector}._domainkey.${input.domain}`,
1771
+ domain: input.domain,
1772
+ selector: input.selector,
1773
+ publicKey: result?.publicKey
1774
+ };
1775
+ }
1776
+ // ==========================================================================
1777
+ // DNS Records
1778
+ // ==========================================================================
1779
+ async getDnsRecords(domain) {
1780
+ const records = await this.request(
1781
+ "GET",
1782
+ `/api/dns/records/${encodeURIComponent(domain)}`
1783
+ );
1784
+ return records ?? [];
1785
+ }
1786
+ // ==========================================================================
1787
+ // Mailbox CRUD
1788
+ // ==========================================================================
1789
+ async createMailbox(input) {
1790
+ const atIndex = input.email.indexOf("@");
1791
+ const localPart = atIndex >= 0 ? input.email.slice(0, atIndex) : input.email;
1792
+ const principal = {
1793
+ name: localPart,
1794
+ type: "individual",
1795
+ description: input.name,
1796
+ emails: [input.email],
1797
+ secrets: [input.password],
1798
+ quota: input.quota
1799
+ };
1800
+ await this.request("POST", "/api/principal", principal);
1801
+ return this.getMailbox(localPart);
1802
+ }
1803
+ async getMailbox(id) {
1804
+ const principal = await this.request(
1805
+ "GET",
1806
+ `/api/principal/${encodeURIComponent(id)}`
1807
+ );
1808
+ return this.principalToMailbox(principal);
1809
+ }
1810
+ async updateMailbox(id, input) {
1811
+ const patch = {};
1812
+ if (input.name !== void 0) patch.description = input.name;
1813
+ if (input.password !== void 0) patch.secrets = [input.password];
1814
+ if (input.quota !== void 0) patch.quota = input.quota;
1815
+ await this.request(
1816
+ "PATCH",
1817
+ `/api/principal/${encodeURIComponent(id)}`,
1818
+ patch
1819
+ );
1820
+ return this.getMailbox(id);
1821
+ }
1822
+ async deleteMailbox(id) {
1823
+ await this.request("DELETE", `/api/principal/${encodeURIComponent(id)}`);
1824
+ }
1825
+ async listMailboxes() {
1826
+ const names = await this.request(
1827
+ "GET",
1828
+ "/api/principal?type=individual"
1829
+ );
1830
+ const mailboxes = await Promise.all(
1831
+ (names ?? []).map((name) => this.getMailbox(name))
1832
+ );
1833
+ return mailboxes;
1834
+ }
1835
+ // ==========================================================================
1836
+ // Principal Mapping Helpers
1837
+ // ==========================================================================
1838
+ principalToUser(principal) {
1839
+ return {
1840
+ id: principal.name,
1841
+ username: principal.name,
1842
+ displayName: principal.description,
1843
+ email: principal.emails?.[0],
1844
+ active: true,
1845
+ groups: principal.memberOf
1846
+ };
1847
+ }
1848
+ principalToGroup(principal) {
1849
+ return {
1850
+ id: principal.name,
1851
+ name: principal.name,
1852
+ displayName: principal.description,
1853
+ description: principal.description,
1854
+ members: principal.members
1855
+ };
1856
+ }
1857
+ principalToMailbox(principal) {
1858
+ return {
1859
+ id: principal.name,
1860
+ name: principal.description ?? principal.name,
1861
+ email: principal.emails?.[0] ?? "",
1862
+ quota: principal.quota,
1863
+ active: true
1864
+ };
1865
+ }
1866
+ }
1867
+ const stalwart = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
1868
+ __proto__: null,
1869
+ StalwartAdapter
1870
+ }, Symbol.toStringTag, { value: "Module" }));
1871
+ function isKanidmOptions(opts) {
1872
+ return opts.type === "kanidm";
1873
+ }
1874
+ function isStalwartOptions(opts) {
1875
+ return opts.type === "stalwart";
1876
+ }
1877
+ function isPostgresOptions(opts) {
1878
+ return opts.type === "postgres";
1879
+ }
1880
+ function isAwsOptions(opts) {
1881
+ return opts.type === "aws";
1882
+ }
1883
+ async function getDirectoryAdapter(options) {
1884
+ if (isKanidmOptions(options)) {
1885
+ const { KanidmAdapter: KanidmAdapter2 } = await Promise.resolve().then(() => kanidm);
1886
+ return new KanidmAdapter2(options);
1887
+ }
1888
+ if (isStalwartOptions(options)) {
1889
+ const { StalwartAdapter: StalwartAdapter2 } = await Promise.resolve().then(() => stalwart);
1890
+ return new StalwartAdapter2(options);
1891
+ }
1892
+ if (isPostgresOptions(options)) {
1893
+ const { PostgresAdapter: PostgresAdapter2 } = await Promise.resolve().then(() => postgres);
1894
+ return new PostgresAdapter2(options);
1895
+ }
1896
+ if (isAwsOptions(options)) {
1897
+ const { AwsAdapter: AwsAdapter2 } = await Promise.resolve().then(() => aws);
1898
+ return new AwsAdapter2(options);
1899
+ }
1900
+ throw new Error(
1901
+ `Unknown directory adapter type: ${options.type}`
1902
+ );
1903
+ }
1904
+ async function getKanidmAdapter(options) {
1905
+ const { KanidmAdapter: KanidmAdapter2 } = await Promise.resolve().then(() => kanidm);
1906
+ return new KanidmAdapter2({ type: "kanidm", ...options });
1907
+ }
1908
+ async function getStalwartAdapter(options) {
1909
+ const { StalwartAdapter: StalwartAdapter2 } = await Promise.resolve().then(() => stalwart);
1910
+ return new StalwartAdapter2({ type: "stalwart", ...options });
1911
+ }
1912
+ async function getPostgresAdapter(options) {
1913
+ const { PostgresAdapter: PostgresAdapter2 } = await Promise.resolve().then(() => postgres);
1914
+ return new PostgresAdapter2({ type: "postgres", ...options });
1915
+ }
1916
+ async function getAwsAdapter(options) {
1917
+ const { AwsAdapter: AwsAdapter2 } = await Promise.resolve().then(() => aws);
1918
+ return new AwsAdapter2({ type: "aws", ...options });
1919
+ }
1920
+ export {
1921
+ AuthenticationError,
1922
+ AwsAdapter,
1923
+ ConflictError,
1924
+ ConnectionError,
1925
+ DirectoryError,
1926
+ KanidmAdapter,
1927
+ NotFoundError,
1928
+ PostgresAdapter,
1929
+ RateLimitError,
1930
+ StalwartAdapter,
1931
+ ValidationError,
1932
+ getAwsAdapter,
1933
+ getDirectoryAdapter,
1934
+ getKanidmAdapter,
1935
+ getPostgresAdapter,
1936
+ getStalwartAdapter,
1937
+ isAwsOptions,
1938
+ isKanidmOptions,
1939
+ isPostgresOptions,
1940
+ isStalwartOptions
1941
+ };
1942
+ //# sourceMappingURL=index.js.map