@hammadj/better-auth-scim 1.5.0-beta.9

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,1308 @@
1
+ import { base64Url } from "@better-auth/utils/base64";
2
+ import { createAuthEndpoint, createAuthMiddleware, defaultKeyHasher } from "better-auth/plugins";
3
+ import { APIError, HIDE_METADATA } from "better-auth";
4
+ import { statusCodes } from "better-call";
5
+ import { generateRandomString, symmetricDecrypt, symmetricEncrypt } from "better-auth/crypto";
6
+ import { APIError as APIError$1, sessionMiddleware } from "better-auth/api";
7
+ import * as z from "zod";
8
+
9
+ //#region src/scim-error.ts
10
+ /**
11
+ * SCIM compliant error
12
+ * See: https://datatracker.ietf.org/doc/html/rfc7644#section-3.12
13
+ */
14
+ var SCIMAPIError = class extends APIError {
15
+ constructor(status = "INTERNAL_SERVER_ERROR", overrides = {}) {
16
+ const body = {
17
+ schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
18
+ status: (typeof status === "number" ? status : statusCodes[status]).toString(),
19
+ detail: overrides.detail,
20
+ ...overrides
21
+ };
22
+ super(status, body);
23
+ this.message = body.detail ?? body.message;
24
+ }
25
+ };
26
+ const SCIMErrorOpenAPISchema = {
27
+ type: "object",
28
+ properties: {
29
+ schemas: {
30
+ type: "array",
31
+ items: { type: "string" }
32
+ },
33
+ status: { type: "string" },
34
+ detail: { type: "string" },
35
+ scimType: { type: "string" }
36
+ }
37
+ };
38
+ const SCIMErrorOpenAPISchemas = {
39
+ "400": {
40
+ description: "Bad Request. Usually due to missing parameters, or invalid parameters",
41
+ content: { "application/json": { schema: SCIMErrorOpenAPISchema } }
42
+ },
43
+ "401": {
44
+ description: "Unauthorized. Due to missing or invalid authentication.",
45
+ content: { "application/json": { schema: SCIMErrorOpenAPISchema } }
46
+ },
47
+ "403": {
48
+ description: "Unauthorized. Due to missing or invalid authentication.",
49
+ content: { "application/json": { schema: SCIMErrorOpenAPISchema } }
50
+ },
51
+ "404": {
52
+ description: "Not Found. The requested resource was not found.",
53
+ content: { "application/json": { schema: SCIMErrorOpenAPISchema } }
54
+ },
55
+ "429": {
56
+ description: "Too Many Requests. You have exceeded the rate limit. Try again later.",
57
+ content: { "application/json": { schema: SCIMErrorOpenAPISchema } }
58
+ },
59
+ "500": {
60
+ description: "Internal Server Error. This is a problem with the server that you cannot fix.",
61
+ content: { "application/json": { schema: SCIMErrorOpenAPISchema } }
62
+ }
63
+ };
64
+
65
+ //#endregion
66
+ //#region src/scim-tokens.ts
67
+ async function storeSCIMToken(ctx, opts, scimToken) {
68
+ if (opts.storeSCIMToken === "encrypted") return await symmetricEncrypt({
69
+ key: ctx.context.secret,
70
+ data: scimToken
71
+ });
72
+ if (opts.storeSCIMToken === "hashed") return await defaultKeyHasher(scimToken);
73
+ if (typeof opts.storeSCIMToken === "object" && "hash" in opts.storeSCIMToken) return await opts.storeSCIMToken.hash(scimToken);
74
+ if (typeof opts.storeSCIMToken === "object" && "encrypt" in opts.storeSCIMToken) return await opts.storeSCIMToken.encrypt(scimToken);
75
+ return scimToken;
76
+ }
77
+ async function verifySCIMToken(ctx, opts, storedSCIMToken, scimToken) {
78
+ if (opts.storeSCIMToken === "encrypted") return await symmetricDecrypt({
79
+ key: ctx.context.secret,
80
+ data: storedSCIMToken
81
+ }) === scimToken;
82
+ if (opts.storeSCIMToken === "hashed") return await defaultKeyHasher(scimToken) === storedSCIMToken;
83
+ if (typeof opts.storeSCIMToken === "object" && "hash" in opts.storeSCIMToken) return await opts.storeSCIMToken.hash(scimToken) === storedSCIMToken;
84
+ if (typeof opts.storeSCIMToken === "object" && "decrypt" in opts.storeSCIMToken) return await opts.storeSCIMToken.decrypt(storedSCIMToken) === scimToken;
85
+ return scimToken === storedSCIMToken;
86
+ }
87
+
88
+ //#endregion
89
+ //#region src/middlewares.ts
90
+ /**
91
+ * The middleware forces the endpoint to have a valid token
92
+ */
93
+ const authMiddlewareFactory = (opts) => createAuthMiddleware(async (ctx) => {
94
+ const authSCIMToken = (ctx.headers?.get("Authorization"))?.replace(/^Bearer\s+/i, "");
95
+ if (!authSCIMToken) throw new SCIMAPIError("UNAUTHORIZED", { detail: "SCIM token is required" });
96
+ const baseScimTokenParts = new TextDecoder().decode(base64Url.decode(authSCIMToken)).split(":");
97
+ const [scimToken, providerId] = baseScimTokenParts;
98
+ const organizationId = baseScimTokenParts.slice(2).join(":");
99
+ if (!scimToken || !providerId) throw new SCIMAPIError("UNAUTHORIZED", { detail: "Invalid SCIM token" });
100
+ let scimProvider = opts.defaultSCIM?.find((p) => {
101
+ if (p.providerId === providerId && !organizationId) return true;
102
+ return !!(p.providerId === providerId && organizationId && p.organizationId === organizationId);
103
+ }) ?? null;
104
+ if (scimProvider) if (scimProvider.scimToken === scimToken) return {
105
+ authSCIMToken: scimProvider.scimToken,
106
+ scimProvider
107
+ };
108
+ else throw new SCIMAPIError("UNAUTHORIZED", { detail: "Invalid SCIM token" });
109
+ scimProvider = await ctx.context.adapter.findOne({
110
+ model: "scimProvider",
111
+ where: [{
112
+ field: "providerId",
113
+ value: providerId
114
+ }, ...organizationId ? [{
115
+ field: "organizationId",
116
+ value: organizationId
117
+ }] : []]
118
+ });
119
+ if (!scimProvider) throw new SCIMAPIError("UNAUTHORIZED", { detail: "Invalid SCIM token" });
120
+ if (!await verifySCIMToken(ctx, opts, scimProvider.scimToken, scimToken)) throw new SCIMAPIError("UNAUTHORIZED", { detail: "Invalid SCIM token" });
121
+ return {
122
+ authSCIMToken: scimToken,
123
+ scimProvider
124
+ };
125
+ });
126
+
127
+ //#endregion
128
+ //#region src/mappings.ts
129
+ const getAccountId = (userName, externalId) => {
130
+ return externalId ?? userName;
131
+ };
132
+ const getFormattedName = (name) => {
133
+ if (name.givenName && name.familyName) return `${name.givenName} ${name.familyName}`;
134
+ if (name.givenName) return name.givenName;
135
+ return name.familyName ?? "";
136
+ };
137
+ const getUserFullName = (email, name) => {
138
+ if (name) {
139
+ const formatted = name.formatted?.trim() ?? "";
140
+ if (formatted.length > 0) return formatted;
141
+ return getFormattedName(name) || email;
142
+ }
143
+ return email;
144
+ };
145
+ const getUserPrimaryEmail = (userName, emails) => {
146
+ return emails?.find((email) => email.primary)?.value ?? emails?.[0]?.value ?? userName;
147
+ };
148
+
149
+ //#endregion
150
+ //#region src/patch-operations.ts
151
+ const identity = (user, op, resources) => {
152
+ return op.value;
153
+ };
154
+ const lowerCase = (user, op, resources) => {
155
+ return op.value.toLowerCase();
156
+ };
157
+ const givenName = (user, op, resources) => {
158
+ const familyName = (resources.user.name ?? user.name).split(" ").slice(1).join(" ").trim();
159
+ const givenName = op.value;
160
+ return getUserFullName(user.email, {
161
+ givenName,
162
+ familyName
163
+ });
164
+ };
165
+ const familyName = (user, op, resources) => {
166
+ const currentName = resources.user.name ?? user.name;
167
+ const givenName = (currentName.split(" ").slice(0, -1).join(" ") || currentName).trim();
168
+ const familyName = op.value;
169
+ return getUserFullName(user.email, {
170
+ givenName,
171
+ familyName
172
+ });
173
+ };
174
+ const userPatchMappings = {
175
+ "/name/formatted": {
176
+ resource: "user",
177
+ target: "name",
178
+ map: identity
179
+ },
180
+ "/name/givenName": {
181
+ resource: "user",
182
+ target: "name",
183
+ map: givenName
184
+ },
185
+ "/name/familyName": {
186
+ resource: "user",
187
+ target: "name",
188
+ map: familyName
189
+ },
190
+ "/externalId": {
191
+ resource: "account",
192
+ target: "accountId",
193
+ map: identity
194
+ },
195
+ "/userName": {
196
+ resource: "user",
197
+ target: "email",
198
+ map: lowerCase
199
+ }
200
+ };
201
+ const normalizePath = (path) => {
202
+ return `/${(path.startsWith("/") ? path.slice(1) : path).replaceAll(".", "/")}`;
203
+ };
204
+ const isNestedObject = (value) => {
205
+ return typeof value === "object" && value !== null && !Array.isArray(value);
206
+ };
207
+ const applyMapping = (user, resources, path, value, op) => {
208
+ const normalizedPath = normalizePath(path);
209
+ const mapping = userPatchMappings[normalizedPath];
210
+ if (!mapping) return;
211
+ const newValue = mapping.map(user, {
212
+ op,
213
+ value,
214
+ path: normalizedPath
215
+ }, resources);
216
+ if (op === "add" && mapping.resource === "user") {
217
+ if (user[mapping.target] === newValue) return;
218
+ }
219
+ resources[mapping.resource][mapping.target] = newValue;
220
+ };
221
+ const applyPatchValue = (user, resources, value, op, path) => {
222
+ if (isNestedObject(value)) for (const [key, nestedValue] of Object.entries(value)) applyPatchValue(user, resources, nestedValue, op, path ? `${path}.${key}` : key);
223
+ else if (path) applyMapping(user, resources, path, value, op);
224
+ };
225
+ const buildUserPatch = (user, operations) => {
226
+ const resources = {
227
+ user: {},
228
+ account: {}
229
+ };
230
+ for (const operation of operations) {
231
+ if (operation.op !== "add" && operation.op !== "replace") continue;
232
+ applyPatchValue(user, resources, operation.value, operation.op, operation.path);
233
+ }
234
+ return resources;
235
+ };
236
+
237
+ //#endregion
238
+ //#region src/user-schemas.ts
239
+ const APIUserSchema = z.object({
240
+ userName: z.string().lowercase(),
241
+ externalId: z.string().optional(),
242
+ name: z.object({
243
+ formatted: z.string().optional(),
244
+ givenName: z.string().optional(),
245
+ familyName: z.string().optional()
246
+ }).optional(),
247
+ emails: z.array(z.object({
248
+ value: z.email(),
249
+ primary: z.boolean().optional()
250
+ })).optional()
251
+ });
252
+ const OpenAPIUserResourceSchema = {
253
+ type: "object",
254
+ properties: {
255
+ id: { type: "string" },
256
+ meta: {
257
+ type: "object",
258
+ properties: {
259
+ resourceType: { type: "string" },
260
+ created: {
261
+ type: "string",
262
+ format: "date-time"
263
+ },
264
+ lastModified: {
265
+ type: "string",
266
+ format: "date-time"
267
+ },
268
+ location: { type: "string" }
269
+ }
270
+ },
271
+ userName: { type: "string" },
272
+ name: {
273
+ type: "object",
274
+ properties: {
275
+ formatted: { type: "string" },
276
+ givenName: { type: "string" },
277
+ familyName: { type: "string" }
278
+ }
279
+ },
280
+ displayName: { type: "string" },
281
+ active: { type: "boolean" },
282
+ emails: {
283
+ type: "array",
284
+ items: {
285
+ type: "object",
286
+ properties: {
287
+ value: { type: "string" },
288
+ primary: { type: "boolean" }
289
+ }
290
+ }
291
+ },
292
+ schemas: {
293
+ type: "array",
294
+ items: { type: "string" }
295
+ }
296
+ }
297
+ };
298
+ const SCIMUserResourceSchema = {
299
+ id: "urn:ietf:params:scim:schemas:core:2.0:User",
300
+ schemas: ["urn:ietf:params:scim:schemas:core:2.0:Schema"],
301
+ name: "User",
302
+ description: "User Account",
303
+ attributes: [
304
+ {
305
+ name: "id",
306
+ type: "string",
307
+ multiValued: false,
308
+ description: "Unique opaque identifier for the User",
309
+ required: false,
310
+ caseExact: true,
311
+ mutability: "readOnly",
312
+ returned: "default",
313
+ uniqueness: "server"
314
+ },
315
+ {
316
+ name: "userName",
317
+ type: "string",
318
+ multiValued: false,
319
+ description: "Unique identifier for the User, typically used by the user to directly authenticate to the service provider",
320
+ required: true,
321
+ caseExact: false,
322
+ mutability: "readWrite",
323
+ returned: "default",
324
+ uniqueness: "server"
325
+ },
326
+ {
327
+ name: "displayName",
328
+ type: "string",
329
+ multiValued: false,
330
+ description: "The name of the User, suitable for display to end-users. The name SHOULD be the full name of the User being described, if known.",
331
+ required: false,
332
+ caseExact: true,
333
+ mutability: "readOnly",
334
+ returned: "default",
335
+ uniqueness: "none"
336
+ },
337
+ {
338
+ name: "active",
339
+ type: "boolean",
340
+ multiValued: false,
341
+ description: "A Boolean value indicating the User's administrative status.",
342
+ required: false,
343
+ mutability: "readOnly",
344
+ returned: "default"
345
+ },
346
+ {
347
+ name: "name",
348
+ type: "complex",
349
+ multiValued: false,
350
+ description: "The components of the user's real name.",
351
+ required: false,
352
+ subAttributes: [
353
+ {
354
+ name: "formatted",
355
+ type: "string",
356
+ multiValued: false,
357
+ description: "The full name, including all middlenames, titles, and suffixes as appropriate, formatted for display(e.g., 'Ms. Barbara J Jensen, III').",
358
+ required: false,
359
+ caseExact: false,
360
+ mutability: "readWrite",
361
+ returned: "default",
362
+ uniqueness: "none"
363
+ },
364
+ {
365
+ name: "familyName",
366
+ type: "string",
367
+ multiValued: false,
368
+ description: "The family name of the User, or last name in most Western languages (e.g., 'Jensen' given the fullname 'Ms. Barbara J Jensen, III').",
369
+ required: false,
370
+ caseExact: false,
371
+ mutability: "readWrite",
372
+ returned: "default",
373
+ uniqueness: "none"
374
+ },
375
+ {
376
+ name: "givenName",
377
+ type: "string",
378
+ multiValued: false,
379
+ description: "The given name of the User, or first name in most Western languages (e.g., 'Barbara' given the full name 'Ms. Barbara J Jensen, III').",
380
+ required: false,
381
+ caseExact: false,
382
+ mutability: "readWrite",
383
+ returned: "default",
384
+ uniqueness: "none"
385
+ }
386
+ ]
387
+ },
388
+ {
389
+ name: "emails",
390
+ type: "complex",
391
+ multiValued: true,
392
+ description: "Email addresses for the user. The value SHOULD be canonicalized by the service provider, e.g., 'bjensen@example.com' instead of 'bjensen@EXAMPLE.COM'. Canonical type values of 'work', 'home', and 'other'.",
393
+ required: false,
394
+ subAttributes: [{
395
+ name: "value",
396
+ type: "string",
397
+ multiValued: false,
398
+ description: "Email addresses for the user. The value SHOULD be canonicalized by the service provider, e.g., 'bjensen@example.com' instead of 'bjensen@EXAMPLE.COM'. Canonical type values of 'work', 'home', and 'other'.",
399
+ required: false,
400
+ caseExact: false,
401
+ mutability: "readWrite",
402
+ returned: "default",
403
+ uniqueness: "server"
404
+ }, {
405
+ name: "primary",
406
+ type: "boolean",
407
+ multiValued: false,
408
+ description: "A Boolean value indicating the 'primary' or preferred attribute value for this attribute, e.g., the preferred mailing address or primary email address. The primary attribute value 'true' MUST appear no more than once.",
409
+ required: false,
410
+ mutability: "readWrite",
411
+ returned: "default"
412
+ }],
413
+ mutability: "readWrite",
414
+ returned: "default",
415
+ uniqueness: "none"
416
+ }
417
+ ],
418
+ meta: {
419
+ resourceType: "Schema",
420
+ location: "/scim/v2/Schemas/urn:ietf:params:scim:schemas:core:2.0:User"
421
+ }
422
+ };
423
+ const SCIMUserResourceType = {
424
+ schemas: ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"],
425
+ id: "User",
426
+ name: "User",
427
+ endpoint: "/Users",
428
+ description: "User Account",
429
+ schema: "urn:ietf:params:scim:schemas:core:2.0:User",
430
+ meta: {
431
+ resourceType: "ResourceType",
432
+ location: "/scim/v2/ResourceTypes/User"
433
+ }
434
+ };
435
+
436
+ //#endregion
437
+ //#region src/scim-filters.ts
438
+ const SCIMOperators = { eq: "eq" };
439
+ const SCIMUserAttributes = { userName: "email" };
440
+ var SCIMParseError = class extends Error {};
441
+ const SCIMFilterRegex = /^\s*(?<attribute>[^\s]+)\s+(?<op>eq|ne|co|sw|ew|pr)\s*(?:(?<value>"[^"]*"|[^\s]+))?\s*$/i;
442
+ const parseSCIMFilter = (filter) => {
443
+ const match = filter.match(SCIMFilterRegex);
444
+ if (!match) throw new SCIMParseError("Invalid filter expression");
445
+ const attribute = match.groups?.attribute;
446
+ const op = match.groups?.op?.toLowerCase();
447
+ const value = match.groups?.value;
448
+ if (!attribute || !op || !value) throw new SCIMParseError("Invalid filter expression");
449
+ const operator = SCIMOperators[op];
450
+ if (!operator) throw new SCIMParseError(`The operator "${op}" is not supported`);
451
+ return {
452
+ attribute,
453
+ operator,
454
+ value
455
+ };
456
+ };
457
+ const parseSCIMUserFilter = (filter) => {
458
+ const { attribute, operator, value } = parseSCIMFilter(filter);
459
+ const filters = [];
460
+ const targetAttribute = SCIMUserAttributes[attribute];
461
+ const resourceAttribute = SCIMUserResourceSchema.attributes.find((attr) => attr.name === attribute);
462
+ if (!targetAttribute || !resourceAttribute) throw new SCIMParseError(`The attribute "${attribute}" is not supported`);
463
+ let finalValue = value.replaceAll("\"", "");
464
+ if (!resourceAttribute.caseExact) finalValue = finalValue.toLowerCase();
465
+ filters.push({
466
+ field: targetAttribute,
467
+ value: finalValue,
468
+ operator
469
+ });
470
+ return filters;
471
+ };
472
+
473
+ //#endregion
474
+ //#region src/scim-metadata.ts
475
+ const MetadataFieldSupportOpenAPISchema = {
476
+ type: "object",
477
+ properties: { supported: { type: "boolean" } }
478
+ };
479
+ const ServiceProviderOpenAPISchema = {
480
+ type: "object",
481
+ properties: {
482
+ patch: MetadataFieldSupportOpenAPISchema,
483
+ bulk: MetadataFieldSupportOpenAPISchema,
484
+ filter: MetadataFieldSupportOpenAPISchema,
485
+ changePassword: MetadataFieldSupportOpenAPISchema,
486
+ sort: MetadataFieldSupportOpenAPISchema,
487
+ etag: MetadataFieldSupportOpenAPISchema,
488
+ authenticationSchemes: {
489
+ type: "array",
490
+ items: {
491
+ type: "object",
492
+ properties: {
493
+ name: { type: "string" },
494
+ description: { type: "string" },
495
+ specUri: { type: "string" },
496
+ type: { type: "string" },
497
+ primary: { type: "boolean" }
498
+ }
499
+ }
500
+ },
501
+ schemas: {
502
+ type: "array",
503
+ items: { type: "string" }
504
+ },
505
+ meta: {
506
+ type: "object",
507
+ properties: { resourceType: { type: "string" } }
508
+ }
509
+ }
510
+ };
511
+ const ResourceTypeOpenAPISchema = {
512
+ type: "object",
513
+ properties: {
514
+ schemas: {
515
+ type: "array",
516
+ items: { type: "string" }
517
+ },
518
+ id: { type: "string" },
519
+ name: { type: "string" },
520
+ endpoint: { type: "string" },
521
+ description: { type: "string" },
522
+ schema: { type: "string" },
523
+ meta: {
524
+ type: "object",
525
+ properties: {
526
+ resourceType: { type: "string" },
527
+ location: { type: "string" }
528
+ }
529
+ }
530
+ }
531
+ };
532
+ const SCIMSchemaAttributesOpenAPISchema = {
533
+ type: "object",
534
+ properties: {
535
+ name: { type: "string" },
536
+ type: { type: "string" },
537
+ multiValued: { type: "boolean" },
538
+ description: { type: "string" },
539
+ required: { type: "boolean" },
540
+ caseExact: { type: "boolean" },
541
+ mutability: { type: "string" },
542
+ returned: { type: "string" },
543
+ uniqueness: { type: "string" }
544
+ }
545
+ };
546
+ const SCIMSchemaOpenAPISchema = {
547
+ type: "object",
548
+ properties: {
549
+ id: { type: "string" },
550
+ schemas: {
551
+ type: "array",
552
+ items: { type: "string" }
553
+ },
554
+ name: { type: "string" },
555
+ description: { type: "string" },
556
+ attributes: {
557
+ type: "array",
558
+ items: {
559
+ ...SCIMSchemaAttributesOpenAPISchema,
560
+ properties: {
561
+ ...SCIMSchemaAttributesOpenAPISchema.properties,
562
+ subAttributes: {
563
+ type: "array",
564
+ items: SCIMSchemaAttributesOpenAPISchema
565
+ }
566
+ }
567
+ }
568
+ },
569
+ meta: {
570
+ type: "object",
571
+ properties: {
572
+ resourceType: { type: "string" },
573
+ location: { type: "string" }
574
+ },
575
+ required: ["resourceType", "location"]
576
+ }
577
+ }
578
+ };
579
+
580
+ //#endregion
581
+ //#region src/utils.ts
582
+ const getResourceURL = (path, baseURL) => {
583
+ const normalizedBaseURL = baseURL.endsWith("/") ? baseURL : `${baseURL}/`;
584
+ const normalizedPath = path.replace(/^\/+/, "");
585
+ return new URL(normalizedPath, normalizedBaseURL).toString();
586
+ };
587
+
588
+ //#endregion
589
+ //#region src/scim-resources.ts
590
+ const createUserResource = (baseURL, user, account) => {
591
+ return {
592
+ id: user.id,
593
+ externalId: account?.accountId,
594
+ meta: {
595
+ resourceType: "User",
596
+ created: user.createdAt,
597
+ lastModified: user.updatedAt,
598
+ location: getResourceURL(`/scim/v2/Users/${user.id}`, baseURL)
599
+ },
600
+ userName: user.email,
601
+ name: { formatted: user.name },
602
+ displayName: user.name,
603
+ active: true,
604
+ emails: [{
605
+ primary: true,
606
+ value: user.email
607
+ }],
608
+ schemas: [SCIMUserResourceSchema.id]
609
+ };
610
+ };
611
+
612
+ //#endregion
613
+ //#region src/routes.ts
614
+ const supportedSCIMSchemas = [SCIMUserResourceSchema];
615
+ const supportedSCIMResourceTypes = [SCIMUserResourceType];
616
+ const supportedMediaTypes = ["application/json", "application/scim+json"];
617
+ const generateSCIMTokenBodySchema = z.object({
618
+ providerId: z.string().meta({ description: "Unique provider identifier" }),
619
+ organizationId: z.string().optional().meta({ description: "Optional organization id" })
620
+ });
621
+ const generateSCIMToken = (opts) => createAuthEndpoint("/scim/generate-token", {
622
+ method: "POST",
623
+ body: generateSCIMTokenBodySchema,
624
+ metadata: { openapi: {
625
+ summary: "Generates a new SCIM token for the given provider",
626
+ description: "Generates a new SCIM token to be used for SCIM operations",
627
+ responses: { "201": {
628
+ description: "SCIM token response",
629
+ content: { "application/json": { schema: {
630
+ type: "object",
631
+ properties: { scimToken: {
632
+ description: "SCIM token",
633
+ type: "string"
634
+ } }
635
+ } } }
636
+ } }
637
+ } },
638
+ use: [sessionMiddleware]
639
+ }, async (ctx) => {
640
+ const { providerId, organizationId } = ctx.body;
641
+ const user = ctx.context.session.user;
642
+ if (providerId.includes(":")) throw new APIError$1("BAD_REQUEST", { message: "Provider id contains forbidden characters" });
643
+ if (organizationId && !ctx.context.hasPlugin("organization")) throw new APIError$1("BAD_REQUEST", { message: "Restricting a token to an organization requires the organization plugin" });
644
+ let member = null;
645
+ if (organizationId) {
646
+ member = await ctx.context.adapter.findOne({
647
+ model: "member",
648
+ where: [{
649
+ field: "userId",
650
+ value: user.id
651
+ }, {
652
+ field: "organizationId",
653
+ value: organizationId
654
+ }]
655
+ });
656
+ if (!member) throw new APIError$1("FORBIDDEN", { message: "You are not a member of the organization" });
657
+ }
658
+ const scimProvider = await ctx.context.adapter.findOne({
659
+ model: "scimProvider",
660
+ where: [{
661
+ field: "providerId",
662
+ value: providerId
663
+ }, ...organizationId ? [{
664
+ field: "organizationId",
665
+ value: organizationId
666
+ }] : []]
667
+ });
668
+ if (scimProvider) await ctx.context.adapter.delete({
669
+ model: "scimProvider",
670
+ where: [{
671
+ field: "id",
672
+ value: scimProvider.id
673
+ }]
674
+ });
675
+ const baseToken = generateRandomString(24);
676
+ const scimToken = base64Url.encode(`${baseToken}:${providerId}${organizationId ? `:${organizationId}` : ""}`);
677
+ if (opts.beforeSCIMTokenGenerated) await opts.beforeSCIMTokenGenerated({
678
+ user,
679
+ member,
680
+ scimToken
681
+ });
682
+ const newSCIMProvider = await ctx.context.adapter.create({
683
+ model: "scimProvider",
684
+ data: {
685
+ providerId,
686
+ organizationId,
687
+ scimToken: await storeSCIMToken(ctx, opts, baseToken)
688
+ }
689
+ });
690
+ if (opts.afterSCIMTokenGenerated) await opts.afterSCIMTokenGenerated({
691
+ user,
692
+ member,
693
+ scimToken,
694
+ scimProvider: newSCIMProvider
695
+ });
696
+ ctx.setStatus(201);
697
+ return ctx.json({ scimToken });
698
+ });
699
+ const createSCIMUser = (authMiddleware) => createAuthEndpoint("/scim/v2/Users", {
700
+ method: "POST",
701
+ body: APIUserSchema,
702
+ metadata: {
703
+ ...HIDE_METADATA,
704
+ allowedMediaTypes: supportedMediaTypes,
705
+ openapi: {
706
+ summary: "Create SCIM user.",
707
+ description: "Provision a new user into the linked organization via SCIM. See https://datatracker.ietf.org/doc/html/rfc7644#section-3.3",
708
+ responses: {
709
+ "201": {
710
+ description: "SCIM user resource",
711
+ content: { "application/json": { schema: OpenAPIUserResourceSchema } }
712
+ },
713
+ ...SCIMErrorOpenAPISchemas
714
+ }
715
+ }
716
+ },
717
+ use: [authMiddleware]
718
+ }, async (ctx) => {
719
+ const body = ctx.body;
720
+ const providerId = ctx.context.scimProvider.providerId;
721
+ const accountId = getAccountId(body.userName, body.externalId);
722
+ if (await ctx.context.adapter.findOne({
723
+ model: "account",
724
+ where: [{
725
+ field: "accountId",
726
+ value: accountId
727
+ }, {
728
+ field: "providerId",
729
+ value: providerId
730
+ }]
731
+ })) throw new SCIMAPIError("CONFLICT", {
732
+ detail: "User already exists",
733
+ scimType: "uniqueness"
734
+ });
735
+ const email = getUserPrimaryEmail(body.userName, body.emails);
736
+ const name = getUserFullName(email, body.name);
737
+ const existingUser = await ctx.context.adapter.findOne({
738
+ model: "user",
739
+ where: [{
740
+ field: "email",
741
+ value: email
742
+ }]
743
+ });
744
+ const createAccount = (userId) => ctx.context.internalAdapter.createAccount({
745
+ userId,
746
+ providerId,
747
+ accountId,
748
+ accessToken: "",
749
+ refreshToken: ""
750
+ });
751
+ const createUser = () => ctx.context.internalAdapter.createUser({
752
+ email,
753
+ name
754
+ });
755
+ const createOrgMembership = async (userId) => {
756
+ const organizationId = ctx.context.scimProvider.organizationId;
757
+ if (organizationId) {
758
+ if (!await ctx.context.adapter.findOne({
759
+ model: "member",
760
+ where: [{
761
+ field: "organizationId",
762
+ value: organizationId
763
+ }, {
764
+ field: "userId",
765
+ value: userId
766
+ }]
767
+ })) return await ctx.context.adapter.create({
768
+ model: "member",
769
+ data: {
770
+ userId,
771
+ role: "member",
772
+ createdAt: /* @__PURE__ */ new Date(),
773
+ organizationId
774
+ }
775
+ });
776
+ }
777
+ };
778
+ let user;
779
+ let account;
780
+ if (existingUser) {
781
+ user = existingUser;
782
+ account = await ctx.context.adapter.transaction(async () => {
783
+ const account = await createAccount(user.id);
784
+ await createOrgMembership(user.id);
785
+ return account;
786
+ });
787
+ } else [user, account] = await ctx.context.adapter.transaction(async () => {
788
+ const user = await createUser();
789
+ const account = await createAccount(user.id);
790
+ await createOrgMembership(user.id);
791
+ return [user, account];
792
+ });
793
+ const userResource = createUserResource(ctx.context.baseURL, user, account);
794
+ ctx.setStatus(201);
795
+ ctx.setHeader("location", userResource.meta.location);
796
+ return ctx.json(userResource);
797
+ });
798
+ const updateSCIMUser = (authMiddleware) => createAuthEndpoint("/scim/v2/Users/:userId", {
799
+ method: "PUT",
800
+ body: APIUserSchema,
801
+ metadata: {
802
+ ...HIDE_METADATA,
803
+ allowedMediaTypes: supportedMediaTypes,
804
+ openapi: {
805
+ summary: "Update SCIM user.",
806
+ description: "Updates an existing user into the linked organization via SCIM. See https://datatracker.ietf.org/doc/html/rfc7644#section-3.3",
807
+ responses: {
808
+ "200": {
809
+ description: "SCIM user resource",
810
+ content: { "application/json": { schema: OpenAPIUserResourceSchema } }
811
+ },
812
+ ...SCIMErrorOpenAPISchemas
813
+ }
814
+ }
815
+ },
816
+ use: [authMiddleware]
817
+ }, async (ctx) => {
818
+ const body = ctx.body;
819
+ const userId = ctx.params.userId;
820
+ const { organizationId, providerId } = ctx.context.scimProvider;
821
+ const accountId = getAccountId(body.userName, body.externalId);
822
+ const { user, account } = await findUserById(ctx.context.adapter, {
823
+ userId,
824
+ providerId,
825
+ organizationId
826
+ });
827
+ if (!user) throw new SCIMAPIError("NOT_FOUND", { detail: "User not found" });
828
+ const [updatedUser, updatedAccount] = await ctx.context.adapter.transaction(async () => {
829
+ const email = getUserPrimaryEmail(body.userName, body.emails);
830
+ const name = getUserFullName(email, body.name);
831
+ return [await ctx.context.internalAdapter.updateUser(userId, {
832
+ email,
833
+ name,
834
+ updatedAt: /* @__PURE__ */ new Date()
835
+ }), await ctx.context.internalAdapter.updateAccount(account.id, {
836
+ accountId,
837
+ updatedAt: /* @__PURE__ */ new Date()
838
+ })];
839
+ });
840
+ const userResource = createUserResource(ctx.context.baseURL, updatedUser, updatedAccount);
841
+ return ctx.json(userResource);
842
+ });
843
+ const listSCIMUsersQuerySchema = z.object({ filter: z.string().optional() }).optional();
844
+ const listSCIMUsers = (authMiddleware) => createAuthEndpoint("/scim/v2/Users", {
845
+ method: "GET",
846
+ query: listSCIMUsersQuerySchema,
847
+ metadata: {
848
+ ...HIDE_METADATA,
849
+ allowedMediaTypes: supportedMediaTypes,
850
+ openapi: {
851
+ summary: "List SCIM users",
852
+ description: "Returns all users provisioned via SCIM for the linked organization. See https://datatracker.ietf.org/doc/html/rfc7644#section-3.4.2",
853
+ responses: {
854
+ "200": {
855
+ description: "SCIM user list",
856
+ content: { "application/json": { schema: {
857
+ type: "object",
858
+ properties: {
859
+ totalResults: { type: "number" },
860
+ itemsPerPage: { type: "number" },
861
+ startIndex: { type: "number" },
862
+ Resources: {
863
+ type: "array",
864
+ items: OpenAPIUserResourceSchema
865
+ }
866
+ }
867
+ } } }
868
+ },
869
+ ...SCIMErrorOpenAPISchemas
870
+ }
871
+ }
872
+ },
873
+ use: [authMiddleware]
874
+ }, async (ctx) => {
875
+ const apiFilters = parseSCIMAPIUserFilter(ctx.query?.filter);
876
+ ctx.context.logger.info("Querying result with filters: ", apiFilters);
877
+ const providerId = ctx.context.scimProvider.providerId;
878
+ const accounts = await ctx.context.adapter.findMany({
879
+ model: "account",
880
+ where: [{
881
+ field: "providerId",
882
+ value: providerId
883
+ }]
884
+ });
885
+ const accountUserIds = accounts.map((account) => account.userId);
886
+ let userFilters = [{
887
+ field: "id",
888
+ value: accountUserIds,
889
+ operator: "in"
890
+ }];
891
+ const organizationId = ctx.context.scimProvider.organizationId;
892
+ if (organizationId) userFilters = [{
893
+ field: "id",
894
+ value: (await ctx.context.adapter.findMany({
895
+ model: "member",
896
+ where: [{
897
+ field: "organizationId",
898
+ value: organizationId
899
+ }, {
900
+ field: "userId",
901
+ value: accountUserIds,
902
+ operator: "in"
903
+ }]
904
+ })).map((member) => member.userId),
905
+ operator: "in"
906
+ }];
907
+ const users = await ctx.context.adapter.findMany({
908
+ model: "user",
909
+ where: [...userFilters, ...apiFilters]
910
+ });
911
+ return ctx.json({
912
+ schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
913
+ totalResults: users.length,
914
+ startIndex: 1,
915
+ itemsPerPage: users.length,
916
+ Resources: users.map((user) => {
917
+ const account = accounts.find((a) => a.userId === user.id);
918
+ return createUserResource(ctx.context.baseURL, user, account);
919
+ })
920
+ });
921
+ });
922
+ const getSCIMUser = (authMiddleware) => createAuthEndpoint("/scim/v2/Users/:userId", {
923
+ method: "GET",
924
+ metadata: {
925
+ ...HIDE_METADATA,
926
+ allowedMediaTypes: supportedMediaTypes,
927
+ openapi: {
928
+ summary: "Get SCIM user details",
929
+ description: "Returns the provisioned SCIM user details. See https://datatracker.ietf.org/doc/html/rfc7644#section-3.4.1",
930
+ responses: {
931
+ "200": {
932
+ description: "SCIM user resource",
933
+ content: { "application/json": { schema: OpenAPIUserResourceSchema } }
934
+ },
935
+ ...SCIMErrorOpenAPISchemas
936
+ }
937
+ }
938
+ },
939
+ use: [authMiddleware]
940
+ }, async (ctx) => {
941
+ const userId = ctx.params.userId;
942
+ const providerId = ctx.context.scimProvider.providerId;
943
+ const organizationId = ctx.context.scimProvider.organizationId;
944
+ const { user, account } = await findUserById(ctx.context.adapter, {
945
+ userId,
946
+ providerId,
947
+ organizationId
948
+ });
949
+ if (!user) throw new SCIMAPIError("NOT_FOUND", { detail: "User not found" });
950
+ return ctx.json(createUserResource(ctx.context.baseURL, user, account));
951
+ });
952
+ const patchSCIMUserBodySchema = z.object({
953
+ schemas: z.array(z.string()).refine((s) => s.includes("urn:ietf:params:scim:api:messages:2.0:PatchOp"), { message: "Invalid schemas for PatchOp" }),
954
+ Operations: z.array(z.object({
955
+ op: z.string().toLowerCase().default("replace").pipe(z.enum([
956
+ "replace",
957
+ "add",
958
+ "remove"
959
+ ])),
960
+ path: z.string().optional(),
961
+ value: z.any()
962
+ }))
963
+ });
964
+ const patchSCIMUser = (authMiddleware) => createAuthEndpoint("/scim/v2/Users/:userId", {
965
+ method: "PATCH",
966
+ body: patchSCIMUserBodySchema,
967
+ metadata: {
968
+ ...HIDE_METADATA,
969
+ allowedMediaTypes: supportedMediaTypes,
970
+ openapi: {
971
+ summary: "Patch SCIM user",
972
+ description: "Updates fields on a SCIM user record",
973
+ responses: {
974
+ "204": { description: "Patch update applied correctly" },
975
+ ...SCIMErrorOpenAPISchemas
976
+ }
977
+ }
978
+ },
979
+ use: [authMiddleware]
980
+ }, async (ctx) => {
981
+ const userId = ctx.params.userId;
982
+ const organizationId = ctx.context.scimProvider.organizationId;
983
+ const providerId = ctx.context.scimProvider.providerId;
984
+ const { user, account } = await findUserById(ctx.context.adapter, {
985
+ userId,
986
+ providerId,
987
+ organizationId
988
+ });
989
+ if (!user) throw new SCIMAPIError("NOT_FOUND", { detail: "User not found" });
990
+ const { user: userPatch, account: accountPatch } = buildUserPatch(user, ctx.body.Operations);
991
+ if (Object.keys(userPatch).length === 0 && Object.keys(accountPatch).length === 0) throw new SCIMAPIError("BAD_REQUEST", { detail: "No valid fields to update" });
992
+ await Promise.all([Object.keys(userPatch).length > 0 ? ctx.context.internalAdapter.updateUser(userId, {
993
+ ...userPatch,
994
+ updatedAt: /* @__PURE__ */ new Date()
995
+ }) : Promise.resolve(), Object.keys(accountPatch).length > 0 ? ctx.context.internalAdapter.updateAccount(account.id, {
996
+ ...accountPatch,
997
+ updatedAt: /* @__PURE__ */ new Date()
998
+ }) : Promise.resolve()]);
999
+ ctx.setStatus(204);
1000
+ });
1001
+ const deleteSCIMUser = (authMiddleware) => createAuthEndpoint("/scim/v2/Users/:userId", {
1002
+ method: "DELETE",
1003
+ metadata: {
1004
+ ...HIDE_METADATA,
1005
+ allowedMediaTypes: [...supportedMediaTypes, ""],
1006
+ openapi: {
1007
+ summary: "Delete SCIM user",
1008
+ description: "Deletes (or deactivates) a user within the linked organization.",
1009
+ responses: {
1010
+ "204": { description: "Delete applied successfully" },
1011
+ ...SCIMErrorOpenAPISchemas
1012
+ }
1013
+ }
1014
+ },
1015
+ use: [authMiddleware]
1016
+ }, async (ctx) => {
1017
+ const userId = ctx.params.userId;
1018
+ const providerId = ctx.context.scimProvider.providerId;
1019
+ const organizationId = ctx.context.scimProvider.organizationId;
1020
+ const { user } = await findUserById(ctx.context.adapter, {
1021
+ userId,
1022
+ providerId,
1023
+ organizationId
1024
+ });
1025
+ if (!user) throw new SCIMAPIError("NOT_FOUND", { detail: "User not found" });
1026
+ await ctx.context.internalAdapter.deleteUser(userId);
1027
+ ctx.setStatus(204);
1028
+ });
1029
+ const getSCIMServiceProviderConfig = createAuthEndpoint("/scim/v2/ServiceProviderConfig", {
1030
+ method: "GET",
1031
+ metadata: {
1032
+ ...HIDE_METADATA,
1033
+ allowedMediaTypes: supportedMediaTypes,
1034
+ openapi: {
1035
+ summary: "SCIM Service Provider Configuration",
1036
+ description: "Standard SCIM metadata endpoint used by identity providers. See https://datatracker.ietf.org/doc/html/rfc7644#section-4",
1037
+ responses: {
1038
+ "200": {
1039
+ description: "SCIM metadata object",
1040
+ content: { "application/json": { schema: ServiceProviderOpenAPISchema } }
1041
+ },
1042
+ ...SCIMErrorOpenAPISchemas
1043
+ }
1044
+ }
1045
+ }
1046
+ }, async (ctx) => {
1047
+ return ctx.json({
1048
+ patch: { supported: true },
1049
+ bulk: { supported: false },
1050
+ filter: { supported: true },
1051
+ changePassword: { supported: false },
1052
+ sort: { supported: false },
1053
+ etag: { supported: false },
1054
+ authenticationSchemes: [{
1055
+ name: "OAuth Bearer Token",
1056
+ description: "Authentication scheme using the Authorization header with a bearer token tied to an organization.",
1057
+ specUri: "http://www.rfc-editor.org/info/rfc6750",
1058
+ type: "oauthbearertoken",
1059
+ primary: true
1060
+ }],
1061
+ schemas: ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
1062
+ meta: { resourceType: "ServiceProviderConfig" }
1063
+ });
1064
+ });
1065
+ const getSCIMSchemas = createAuthEndpoint("/scim/v2/Schemas", {
1066
+ method: "GET",
1067
+ metadata: {
1068
+ ...HIDE_METADATA,
1069
+ allowedMediaTypes: supportedMediaTypes,
1070
+ openapi: {
1071
+ summary: "SCIM Service Provider Configuration Schemas",
1072
+ description: "Standard SCIM metadata endpoint used by identity providers to acquire information about supported schemas. See https://datatracker.ietf.org/doc/html/rfc7644#section-4",
1073
+ responses: {
1074
+ "200": {
1075
+ description: "SCIM metadata object",
1076
+ content: { "application/json": { schema: {
1077
+ type: "array",
1078
+ items: SCIMSchemaOpenAPISchema
1079
+ } } }
1080
+ },
1081
+ ...SCIMErrorOpenAPISchemas
1082
+ }
1083
+ }
1084
+ }
1085
+ }, async (ctx) => {
1086
+ return ctx.json({
1087
+ totalResults: supportedSCIMSchemas.length,
1088
+ itemsPerPage: supportedSCIMSchemas.length,
1089
+ startIndex: 1,
1090
+ schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
1091
+ Resources: supportedSCIMSchemas.map((s) => {
1092
+ return {
1093
+ ...s,
1094
+ meta: {
1095
+ ...s.meta,
1096
+ location: getResourceURL(s.meta.location, ctx.context.baseURL)
1097
+ }
1098
+ };
1099
+ })
1100
+ });
1101
+ });
1102
+ const getSCIMSchema = createAuthEndpoint("/scim/v2/Schemas/:schemaId", {
1103
+ method: "GET",
1104
+ metadata: {
1105
+ ...HIDE_METADATA,
1106
+ allowedMediaTypes: supportedMediaTypes,
1107
+ openapi: {
1108
+ summary: "SCIM a Service Provider Configuration Schema",
1109
+ description: "Standard SCIM metadata endpoint used by identity providers to acquire information about a given schema. See https://datatracker.ietf.org/doc/html/rfc7644#section-4",
1110
+ responses: {
1111
+ "200": {
1112
+ description: "SCIM metadata object",
1113
+ content: { "application/json": { schema: SCIMSchemaOpenAPISchema } }
1114
+ },
1115
+ ...SCIMErrorOpenAPISchemas
1116
+ }
1117
+ }
1118
+ }
1119
+ }, async (ctx) => {
1120
+ const schema = supportedSCIMSchemas.find((s) => s.id === ctx.params.schemaId);
1121
+ if (!schema) throw new SCIMAPIError("NOT_FOUND", { detail: "Schema not found" });
1122
+ return ctx.json({
1123
+ ...schema,
1124
+ meta: {
1125
+ ...schema.meta,
1126
+ location: getResourceURL(schema.meta.location, ctx.context.baseURL)
1127
+ }
1128
+ });
1129
+ });
1130
+ const getSCIMResourceTypes = createAuthEndpoint("/scim/v2/ResourceTypes", {
1131
+ method: "GET",
1132
+ metadata: {
1133
+ ...HIDE_METADATA,
1134
+ allowedMediaTypes: supportedMediaTypes,
1135
+ openapi: {
1136
+ summary: "SCIM Service Provider Supported Resource Types",
1137
+ description: "Standard SCIM metadata endpoint used by identity providers to get a list of server supported types. See https://datatracker.ietf.org/doc/html/rfc7644#section-4",
1138
+ responses: {
1139
+ "200": {
1140
+ description: "SCIM metadata object",
1141
+ content: { "application/json": { schema: {
1142
+ type: "object",
1143
+ properties: {
1144
+ totalResults: { type: "number" },
1145
+ itemsPerPage: { type: "number" },
1146
+ startIndex: { type: "number" },
1147
+ Resources: {
1148
+ type: "array",
1149
+ items: ResourceTypeOpenAPISchema
1150
+ }
1151
+ }
1152
+ } } }
1153
+ },
1154
+ ...SCIMErrorOpenAPISchemas
1155
+ }
1156
+ }
1157
+ }
1158
+ }, async (ctx) => {
1159
+ return ctx.json({
1160
+ totalResults: supportedSCIMResourceTypes.length,
1161
+ itemsPerPage: supportedSCIMResourceTypes.length,
1162
+ startIndex: 1,
1163
+ schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
1164
+ Resources: supportedSCIMResourceTypes.map((s) => {
1165
+ return {
1166
+ ...s,
1167
+ meta: {
1168
+ ...s.meta,
1169
+ location: getResourceURL(s.meta.location, ctx.context.baseURL)
1170
+ }
1171
+ };
1172
+ })
1173
+ });
1174
+ });
1175
+ const getSCIMResourceType = createAuthEndpoint("/scim/v2/ResourceTypes/:resourceTypeId", {
1176
+ method: "GET",
1177
+ metadata: {
1178
+ ...HIDE_METADATA,
1179
+ allowedMediaTypes: supportedMediaTypes,
1180
+ openapi: {
1181
+ summary: "SCIM Service Provider Supported Resource Type",
1182
+ description: "Standard SCIM metadata endpoint used by identity providers to get a server supported type. See https://datatracker.ietf.org/doc/html/rfc7644#section-4",
1183
+ responses: {
1184
+ "200": {
1185
+ description: "SCIM metadata object",
1186
+ content: { "application/json": { schema: ResourceTypeOpenAPISchema } }
1187
+ },
1188
+ ...SCIMErrorOpenAPISchemas
1189
+ }
1190
+ }
1191
+ }
1192
+ }, async (ctx) => {
1193
+ const resourceType = supportedSCIMResourceTypes.find((s) => s.id === ctx.params.resourceTypeId);
1194
+ if (!resourceType) throw new SCIMAPIError("NOT_FOUND", { detail: "Resource type not found" });
1195
+ return ctx.json({
1196
+ ...resourceType,
1197
+ meta: {
1198
+ ...resourceType.meta,
1199
+ location: getResourceURL(resourceType.meta.location, ctx.context.baseURL)
1200
+ }
1201
+ });
1202
+ });
1203
+ const findUserById = async (adapter, { userId, providerId, organizationId }) => {
1204
+ const account = await adapter.findOne({
1205
+ model: "account",
1206
+ where: [{
1207
+ field: "userId",
1208
+ value: userId
1209
+ }, {
1210
+ field: "providerId",
1211
+ value: providerId
1212
+ }]
1213
+ });
1214
+ if (!account) return {
1215
+ user: null,
1216
+ account: null
1217
+ };
1218
+ let member = null;
1219
+ if (organizationId) member = await adapter.findOne({
1220
+ model: "member",
1221
+ where: [{
1222
+ field: "organizationId",
1223
+ value: organizationId
1224
+ }, {
1225
+ field: "userId",
1226
+ value: userId
1227
+ }]
1228
+ });
1229
+ if (organizationId && !member) return {
1230
+ user: null,
1231
+ account: null
1232
+ };
1233
+ const user = await adapter.findOne({
1234
+ model: "user",
1235
+ where: [{
1236
+ field: "id",
1237
+ value: userId
1238
+ }]
1239
+ });
1240
+ if (!user) return {
1241
+ user: null,
1242
+ account: null
1243
+ };
1244
+ return {
1245
+ user,
1246
+ account
1247
+ };
1248
+ };
1249
+ const parseSCIMAPIUserFilter = (filter) => {
1250
+ let filters = [];
1251
+ try {
1252
+ filters = filter ? parseSCIMUserFilter(filter) : [];
1253
+ } catch (error) {
1254
+ throw new SCIMAPIError("BAD_REQUEST", {
1255
+ detail: error instanceof SCIMParseError ? error.message : "Invalid SCIM filter",
1256
+ scimType: "invalidFilter"
1257
+ });
1258
+ }
1259
+ return filters;
1260
+ };
1261
+
1262
+ //#endregion
1263
+ //#region src/index.ts
1264
+ const scim = (options) => {
1265
+ const opts = {
1266
+ storeSCIMToken: "plain",
1267
+ ...options
1268
+ };
1269
+ const authMiddleware = authMiddlewareFactory(opts);
1270
+ return {
1271
+ id: "scim",
1272
+ endpoints: {
1273
+ generateSCIMToken: generateSCIMToken(opts),
1274
+ getSCIMUser: getSCIMUser(authMiddleware),
1275
+ createSCIMUser: createSCIMUser(authMiddleware),
1276
+ patchSCIMUser: patchSCIMUser(authMiddleware),
1277
+ deleteSCIMUser: deleteSCIMUser(authMiddleware),
1278
+ updateSCIMUser: updateSCIMUser(authMiddleware),
1279
+ listSCIMUsers: listSCIMUsers(authMiddleware),
1280
+ getSCIMServiceProviderConfig,
1281
+ getSCIMSchemas,
1282
+ getSCIMSchema,
1283
+ getSCIMResourceTypes,
1284
+ getSCIMResourceType
1285
+ },
1286
+ schema: { scimProvider: { fields: {
1287
+ providerId: {
1288
+ type: "string",
1289
+ required: true,
1290
+ unique: true
1291
+ },
1292
+ scimToken: {
1293
+ type: "string",
1294
+ required: true,
1295
+ unique: true
1296
+ },
1297
+ organizationId: {
1298
+ type: "string",
1299
+ required: false
1300
+ }
1301
+ } } },
1302
+ options
1303
+ };
1304
+ };
1305
+
1306
+ //#endregion
1307
+ export { scim };
1308
+ //# sourceMappingURL=index.mjs.map