@better-auth/scim 1.6.0-beta.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { t as PACKAGE_VERSION } from "./version-BqMHXjG-.mjs";
1
+ import { t as PACKAGE_VERSION } from "./version-CfoAqE_A.mjs";
2
2
  import { base64Url } from "@better-auth/utils/base64";
3
3
  import { APIError, createAuthEndpoint, createAuthMiddleware, sessionMiddleware } from "better-auth/api";
4
4
  import { APIError as APIError$1, HIDE_METADATA } from "better-auth";
@@ -614,7 +614,21 @@ const generateSCIMTokenBodySchema = z.object({
614
614
  });
615
615
  const getSCIMProviderConnectionQuerySchema = z.object({ providerId: z.string() });
616
616
  const deleteSCIMProviderConnectionBodySchema = z.object({ providerId: z.string() });
617
- async function getSCIMUserOrgIds(ctx, userId) {
617
+ function parseMemberRoles(role) {
618
+ return role.split(",").map((entry) => entry.trim()).filter(Boolean);
619
+ }
620
+ function hasRequiredRole(memberRole, requiredRole) {
621
+ return !requiredRole.length || parseMemberRoles(memberRole).some((role) => requiredRole.includes(role));
622
+ }
623
+ function resolveRequiredRoles(ctx, opts) {
624
+ if (opts.requiredRole) return opts.requiredRole;
625
+ const creatorRole = ctx.context.getPlugin("organization")?.options?.creatorRole;
626
+ return Array.from(new Set(["admin", creatorRole ?? "owner"]));
627
+ }
628
+ function isProviderOwnershipEnabled(opts) {
629
+ return opts.providerOwnership?.enabled ?? false;
630
+ }
631
+ async function getSCIMUserOrgMemberships(ctx, userId) {
618
632
  const members = await ctx.context.adapter.findMany({
619
633
  model: "member",
620
634
  where: [{
@@ -622,7 +636,7 @@ async function getSCIMUserOrgIds(ctx, userId) {
622
636
  value: userId
623
637
  }]
624
638
  });
625
- return new Set(members.map((m) => m.organizationId));
639
+ return new Map(members.map((member) => [member.organizationId, parseMemberRoles(member.role)]));
626
640
  }
627
641
  function normalizeSCIMProvider(provider) {
628
642
  return {
@@ -643,13 +657,15 @@ async function findOrganizationMember(ctx, userId, organizationId) {
643
657
  }]
644
658
  });
645
659
  }
646
- async function assertSCIMProviderAccess(ctx, userId, provider) {
660
+ async function assertSCIMProviderAccess(ctx, userId, provider, requiredRole) {
647
661
  if (provider.organizationId) {
648
662
  if (!ctx.context.hasPlugin("organization")) throw new APIError("FORBIDDEN", { message: "Organization plugin is required to access this SCIM provider" });
649
- if (!await findOrganizationMember(ctx, userId, provider.organizationId)) throw new APIError("FORBIDDEN", { message: "You must be a member of the organization to access this provider" });
663
+ const member = await findOrganizationMember(ctx, userId, provider.organizationId);
664
+ if (!member) throw new APIError("FORBIDDEN", { message: "You must be a member of the organization to access this provider" });
665
+ if (!hasRequiredRole(member.role, requiredRole)) throw new APIError("FORBIDDEN", { message: "Insufficient role for this operation" });
650
666
  } else if (provider.userId && provider.userId !== userId) throw new APIError("FORBIDDEN", { message: "You must be the owner to access this provider" });
651
667
  }
652
- async function checkSCIMProviderAccess(ctx, userId, providerId) {
668
+ async function checkSCIMProviderAccess(ctx, userId, providerId, requiredRole) {
653
669
  const provider = await ctx.context.adapter.findOne({
654
670
  model: "scimProvider",
655
671
  where: [{
@@ -658,7 +674,7 @@ async function checkSCIMProviderAccess(ctx, userId, providerId) {
658
674
  }]
659
675
  });
660
676
  if (!provider) throw new APIError("NOT_FOUND", { message: "SCIM provider not found" });
661
- await assertSCIMProviderAccess(ctx, userId, provider);
677
+ await assertSCIMProviderAccess(ctx, userId, provider, requiredRole);
662
678
  return provider;
663
679
  }
664
680
  const generateSCIMToken = (opts) => createAuthEndpoint("/scim/generate-token", {
@@ -682,12 +698,14 @@ const generateSCIMToken = (opts) => createAuthEndpoint("/scim/generate-token", {
682
698
  }, async (ctx) => {
683
699
  const { providerId, organizationId } = ctx.body;
684
700
  const user = ctx.context.session.user;
701
+ const requiredRole = resolveRequiredRoles(ctx, opts);
685
702
  if (providerId.includes(":")) throw new APIError("BAD_REQUEST", { message: "Provider id contains forbidden characters" });
686
703
  if (organizationId && !ctx.context.hasPlugin("organization")) throw new APIError("BAD_REQUEST", { message: "Restricting a token to an organization requires the organization plugin" });
687
704
  let member = null;
688
705
  if (organizationId) {
689
706
  member = await findOrganizationMember(ctx, user.id, organizationId);
690
707
  if (!member) throw new APIError("FORBIDDEN", { message: "You are not a member of the organization" });
708
+ if (!hasRequiredRole(member.role, requiredRole)) throw new APIError("FORBIDDEN", { message: "Insufficient role for this operation" });
691
709
  }
692
710
  const scimProvider = await ctx.context.adapter.findOne({
693
711
  model: "scimProvider",
@@ -700,7 +718,7 @@ const generateSCIMToken = (opts) => createAuthEndpoint("/scim/generate-token", {
700
718
  }] : []]
701
719
  });
702
720
  if (scimProvider) {
703
- await assertSCIMProviderAccess(ctx, user.id, scimProvider);
721
+ await assertSCIMProviderAccess(ctx, user.id, scimProvider, requiredRole);
704
722
  await ctx.context.adapter.delete({
705
723
  model: "scimProvider",
706
724
  where: [{
@@ -722,7 +740,7 @@ const generateSCIMToken = (opts) => createAuthEndpoint("/scim/generate-token", {
722
740
  providerId,
723
741
  organizationId,
724
742
  scimToken: await storeSCIMToken(ctx, opts, baseToken),
725
- ...opts.providerOwnership?.enabled ? { userId: user.id } : {}
743
+ ...isProviderOwnershipEnabled(opts) ? { userId: user.id } : {}
726
744
  }
727
745
  });
728
746
  if (opts.afterSCIMTokenGenerated) await opts.afterSCIMTokenGenerated({
@@ -734,13 +752,13 @@ const generateSCIMToken = (opts) => createAuthEndpoint("/scim/generate-token", {
734
752
  ctx.setStatus(201);
735
753
  return ctx.json({ scimToken });
736
754
  });
737
- const listSCIMProviderConnections = () => createAuthEndpoint("/scim/list-provider-connections", {
755
+ const listSCIMProviderConnections = (opts) => createAuthEndpoint("/scim/list-provider-connections", {
738
756
  method: "GET",
739
757
  use: [sessionMiddleware],
740
758
  metadata: { openapi: {
741
759
  operationId: "listSCIMProviderConnections",
742
760
  summary: "List SCIM providers",
743
- description: "Returns SCIM providers for organizations the user is a member of.",
761
+ description: "Returns SCIM providers the user owns or has the required org role for.",
744
762
  responses: { "200": {
745
763
  description: "List of SCIM providers",
746
764
  content: { "application/json": { schema: {
@@ -764,15 +782,18 @@ const listSCIMProviderConnections = () => createAuthEndpoint("/scim/list-provide
764
782
  } }
765
783
  }, async (ctx) => {
766
784
  const userId = ctx.context.session.user.id;
767
- const userOrgIds = ctx.context.hasPlugin("organization") ? await getSCIMUserOrgIds(ctx, userId) : /* @__PURE__ */ new Set();
785
+ const requiredRole = resolveRequiredRoles(ctx, opts);
786
+ const orgMemberships = ctx.context.hasPlugin("organization") ? await getSCIMUserOrgMemberships(ctx, userId) : /* @__PURE__ */ new Map();
768
787
  const providers = (await ctx.context.adapter.findMany({ model: "scimProvider" })).filter((p) => {
769
- if (p.organizationId) return userOrgIds.has(p.organizationId);
770
- if (p.userId === userId) return true;
771
- return !p.userId;
788
+ if (p.organizationId) {
789
+ const roles = orgMemberships.get(p.organizationId);
790
+ return roles ? !requiredRole.length || roles.some((role) => requiredRole.includes(role)) : false;
791
+ }
792
+ return p.userId === userId || !p.userId;
772
793
  }).map((p) => normalizeSCIMProvider(p));
773
794
  return ctx.json({ providers });
774
795
  });
775
- const getSCIMProviderConnection = () => createAuthEndpoint("/scim/get-provider-connection", {
796
+ const getSCIMProviderConnection = (opts) => createAuthEndpoint("/scim/get-provider-connection", {
776
797
  method: "GET",
777
798
  use: [sessionMiddleware],
778
799
  query: getSCIMProviderConnectionQuerySchema,
@@ -802,10 +823,10 @@ const getSCIMProviderConnection = () => createAuthEndpoint("/scim/get-provider-c
802
823
  }, async (ctx) => {
803
824
  const { providerId } = ctx.query;
804
825
  const userId = ctx.context.session.user.id;
805
- const provider = await checkSCIMProviderAccess(ctx, userId, providerId);
826
+ const provider = await checkSCIMProviderAccess(ctx, userId, providerId, resolveRequiredRoles(ctx, opts));
806
827
  return ctx.json(normalizeSCIMProvider(provider));
807
828
  });
808
- const deleteSCIMProviderConnection = () => createAuthEndpoint("/scim/delete-provider-connection", {
829
+ const deleteSCIMProviderConnection = (opts) => createAuthEndpoint("/scim/delete-provider-connection", {
809
830
  method: "POST",
810
831
  use: [sessionMiddleware],
811
832
  body: deleteSCIMProviderConnectionBodySchema,
@@ -828,7 +849,7 @@ const deleteSCIMProviderConnection = () => createAuthEndpoint("/scim/delete-prov
828
849
  }, async (ctx) => {
829
850
  const { providerId } = ctx.body;
830
851
  const userId = ctx.context.session.user.id;
831
- await checkSCIMProviderAccess(ctx, userId, providerId);
852
+ await checkSCIMProviderAccess(ctx, userId, providerId, resolveRequiredRoles(ctx, opts));
832
853
  await ctx.context.adapter.delete({
833
854
  model: "scimProvider",
834
855
  where: [{
@@ -1417,18 +1438,18 @@ const parseSCIMAPIUserFilter = (filter) => {
1417
1438
  const scim = (options) => {
1418
1439
  const opts = {
1419
1440
  storeSCIMToken: "plain",
1420
- providerOwnership: { enabled: false },
1421
1441
  ...options
1422
1442
  };
1443
+ const providerOwnershipEnabled = options?.providerOwnership?.enabled ?? false;
1423
1444
  const authMiddleware = authMiddlewareFactory(opts);
1424
1445
  return {
1425
1446
  id: "scim",
1426
1447
  version: PACKAGE_VERSION,
1427
1448
  endpoints: {
1428
1449
  generateSCIMToken: generateSCIMToken(opts),
1429
- listSCIMProviderConnections: listSCIMProviderConnections(),
1430
- getSCIMProviderConnection: getSCIMProviderConnection(),
1431
- deleteSCIMProviderConnection: deleteSCIMProviderConnection(),
1450
+ listSCIMProviderConnections: listSCIMProviderConnections(opts),
1451
+ getSCIMProviderConnection: getSCIMProviderConnection(opts),
1452
+ deleteSCIMProviderConnection: deleteSCIMProviderConnection(opts),
1432
1453
  getSCIMUser: getSCIMUser(authMiddleware),
1433
1454
  createSCIMUser: createSCIMUser(authMiddleware),
1434
1455
  patchSCIMUser: patchSCIMUser(authMiddleware),
@@ -1456,7 +1477,7 @@ const scim = (options) => {
1456
1477
  type: "string",
1457
1478
  required: false
1458
1479
  },
1459
- ...opts.providerOwnership?.enabled ? { userId: {
1480
+ ...providerOwnershipEnabled ? { userId: {
1460
1481
  type: "string",
1461
1482
  required: false
1462
1483
  } } : {}
@@ -1,5 +1,5 @@
1
1
  //#endregion
2
2
  //#region src/version.ts
3
- const PACKAGE_VERSION = "1.6.0-beta.0";
3
+ const PACKAGE_VERSION = "1.6.1";
4
4
  //#endregion
5
5
  export { PACKAGE_VERSION as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/scim",
3
- "version": "1.6.0-beta.0",
3
+ "version": "1.6.1",
4
4
  "description": "SCIM plugin for Better Auth",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -54,14 +54,14 @@
54
54
  },
55
55
  "devDependencies": {
56
56
  "tsdown": "0.21.1",
57
- "@better-auth/core": "1.6.0-beta.0",
58
- "@better-auth/sso": "1.6.0-beta.0"
57
+ "@better-auth/core": "1.6.1",
58
+ "@better-auth/sso": "1.6.1"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "@better-auth/utils": "0.4.0",
62
- "better-call": "2.0.3",
63
- "@better-auth/core": "^1.6.0-beta.0",
64
- "better-auth": "^1.6.0-beta.0"
62
+ "better-call": "1.3.5",
63
+ "@better-auth/core": "^1.6.1",
64
+ "better-auth": "^1.6.1"
65
65
  },
66
66
  "scripts": {
67
67
  "build": "tsdown",