@mastra/auth-workos 1.1.2-alpha.0 → 1.2.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,8 +2,8 @@ import { verifyJwks } from '@mastra/auth';
2
2
  import { MastraAuthProvider } from '@mastra/core/server';
3
3
  import { CookieSessionStorage, AuthService, sessionEncryption } from '@workos/authkit-session';
4
4
  import { WorkOS, GeneratePortalLinkIntent } from '@workos-inc/node';
5
- import { resolvePermissionsFromMapping, matchesPermission } from '@mastra/core/auth/ee';
6
5
  import { LRUCache } from 'lru-cache';
6
+ import { resolvePermissionsFromMapping, matchesPermission, FGADeniedError } from '@mastra/core/auth/ee';
7
7
 
8
8
  // src/auth-provider.ts
9
9
  var WebSessionStorage = class extends CookieSessionStorage {
@@ -52,6 +52,8 @@ function mapWorkOSUserToEEUser(user) {
52
52
 
53
53
  // src/auth-provider.ts
54
54
  var DEV_COOKIE_PASSWORD = crypto.randomUUID() + crypto.randomUUID();
55
+ var MEMBERSHIP_CACHE_TTL_MS = 60 * 1e3;
56
+ var MEMBERSHIP_CACHE_MAX_SIZE = 1e3;
55
57
  var MastraAuthWorkos = class extends MastraAuthProvider {
56
58
  workos;
57
59
  clientId;
@@ -59,6 +61,11 @@ var MastraAuthWorkos = class extends MastraAuthProvider {
59
61
  ssoConfig;
60
62
  authService;
61
63
  config;
64
+ fetchMemberships;
65
+ trustJwtClaims;
66
+ jwtClaimOptions;
67
+ mapJwtPayloadToUser;
68
+ membershipCache;
62
69
  constructor(options) {
63
70
  super({ name: options?.name ?? "workos" });
64
71
  const apiKey = options?.apiKey ?? process.env.WORKOS_API_KEY;
@@ -83,6 +90,14 @@ var MastraAuthWorkos = class extends MastraAuthProvider {
83
90
  this.clientId = clientId;
84
91
  this.redirectUri = redirectUri;
85
92
  this.ssoConfig = options?.sso;
93
+ this.fetchMemberships = options?.fetchMemberships ?? false;
94
+ this.trustJwtClaims = options?.trustJwtClaims ?? false;
95
+ this.jwtClaimOptions = options?.jwtClaims;
96
+ this.mapJwtPayloadToUser = options?.mapJwtPayloadToUser;
97
+ this.membershipCache = new LRUCache({
98
+ max: MEMBERSHIP_CACHE_MAX_SIZE,
99
+ ttl: MEMBERSHIP_CACHE_TTL_MS
100
+ });
86
101
  this.workos = new WorkOS(apiKey, { clientId });
87
102
  this.config = {
88
103
  clientId,
@@ -119,27 +134,57 @@ var MastraAuthWorkos = class extends MastraAuthProvider {
119
134
  const rawRequest = "raw" in request ? request.raw : request;
120
135
  const { auth } = await this.authService.withAuth(rawRequest);
121
136
  if (auth.user) {
137
+ let memberships;
138
+ if (this.fetchMemberships) {
139
+ try {
140
+ memberships = await this.getMemberships(auth.user.id);
141
+ } catch {
142
+ }
143
+ }
122
144
  return {
123
145
  ...mapWorkOSUserToEEUser(auth.user),
124
146
  workosId: auth.user.id,
125
- organizationId: auth.organizationId
126
- // Note: memberships not available from session, fetch if needed
147
+ organizationId: auth.organizationId,
148
+ memberships
127
149
  };
128
150
  }
129
151
  if (token) {
130
152
  const jwksUri = this.workos.userManagement.getJwksUrl(this.clientId);
131
153
  const payload = await verifyJwks(token, jwksUri);
154
+ const jwtUser = this.resolveJwtPayloadUser(payload);
155
+ if (this.trustJwtClaims && jwtUser?.id && jwtUser?.workosId) {
156
+ return await this.attachMembershipsIfNeeded(jwtUser);
157
+ }
132
158
  if (payload?.sub) {
133
- const user = await this.workos.userManagement.getUser(payload.sub);
134
- const memberships = await this.workos.userManagement.listOrganizationMemberships({
135
- userId: user.id
136
- });
137
- return {
138
- ...mapWorkOSUserToEEUser(user),
139
- workosId: user.id,
140
- organizationId: memberships.data[0]?.organizationId,
141
- memberships: memberships.data
142
- };
159
+ try {
160
+ const user = await this.workos.userManagement.getUser(payload.sub);
161
+ let memberships;
162
+ if (this.fetchMemberships) {
163
+ try {
164
+ memberships = await this.getMemberships(user.id);
165
+ } catch {
166
+ memberships = void 0;
167
+ }
168
+ }
169
+ return this.mergeJwtPayloadUser(
170
+ {
171
+ ...mapWorkOSUserToEEUser(user),
172
+ workosId: user.id,
173
+ organizationId: this.getSingleMembershipOrganizationId(memberships),
174
+ memberships
175
+ },
176
+ jwtUser,
177
+ { trustOrganizationClaims: this.trustJwtClaims }
178
+ );
179
+ } catch {
180
+ if (this.trustJwtClaims && jwtUser?.id && jwtUser?.workosId) {
181
+ return await this.attachMembershipsIfNeeded(jwtUser);
182
+ }
183
+ return null;
184
+ }
185
+ }
186
+ if (this.trustJwtClaims && jwtUser?.id && jwtUser?.workosId) {
187
+ return await this.attachMembershipsIfNeeded(jwtUser);
143
188
  }
144
189
  }
145
190
  return null;
@@ -166,19 +211,19 @@ var MastraAuthWorkos = class extends MastraAuthProvider {
166
211
  return null;
167
212
  }
168
213
  let organizationId = auth.organizationId;
169
- if (!organizationId) {
214
+ let memberships;
215
+ if (this.fetchMemberships) {
170
216
  try {
171
- const memberships = await this.workos.userManagement.listOrganizationMemberships({
172
- userId: auth.user.id
173
- });
174
- organizationId = memberships.data[0]?.organizationId;
217
+ memberships = await this.getMemberships(auth.user.id);
218
+ organizationId ??= this.getSingleMembershipOrganizationId(memberships);
175
219
  } catch {
176
220
  }
177
221
  }
178
222
  const user = {
179
223
  ...mapWorkOSUserToEEUser(auth.user),
180
224
  workosId: auth.user.id,
181
- organizationId
225
+ organizationId,
226
+ memberships
182
227
  };
183
228
  if (refreshedSessionData) {
184
229
  user._refreshedSessionData = refreshedSessionData;
@@ -208,6 +253,124 @@ var MastraAuthWorkos = class extends MastraAuthProvider {
208
253
  getUserProfileUrl(user) {
209
254
  return `/profile/${user.id}`;
210
255
  }
256
+ async getMemberships(userId) {
257
+ const cached = this.membershipCache.get(userId);
258
+ if (cached) {
259
+ return cached;
260
+ }
261
+ try {
262
+ const response = await this.workos.userManagement.listOrganizationMemberships({
263
+ userId
264
+ });
265
+ const memberships = await response.autoPagination();
266
+ this.membershipCache.set(userId, memberships);
267
+ return memberships;
268
+ } catch (error) {
269
+ this.membershipCache.delete(userId);
270
+ throw error;
271
+ }
272
+ }
273
+ async attachMembershipsIfNeeded(user) {
274
+ if (!this.fetchMemberships || user.organizationMembershipId) {
275
+ return user;
276
+ }
277
+ try {
278
+ const memberships = await this.getMemberships(user.workosId);
279
+ return {
280
+ ...user,
281
+ organizationId: user.organizationId ?? this.getSingleMembershipOrganizationId(memberships),
282
+ memberships
283
+ };
284
+ } catch {
285
+ return user;
286
+ }
287
+ }
288
+ getSingleMembershipOrganizationId(memberships) {
289
+ return memberships?.length === 1 ? memberships[0]?.organizationId : void 0;
290
+ }
291
+ resolveJwtPayloadUser(payload) {
292
+ if (!payload) {
293
+ return null;
294
+ }
295
+ const mappedClaims = this.buildUserFromJwtClaims(payload);
296
+ const customMappedClaims = this.mapJwtPayloadToUser?.(payload) ?? void 0;
297
+ const combined = {
298
+ ...payload,
299
+ ...mappedClaims ?? {},
300
+ ...customMappedClaims ?? {}
301
+ };
302
+ const id = typeof combined.id === "string" ? combined.id : void 0;
303
+ const workosId = typeof combined.workosId === "string" ? combined.workosId : id;
304
+ if (!id || !workosId) {
305
+ return null;
306
+ }
307
+ const metadata = combined.metadata && typeof combined.metadata === "object" && !Array.isArray(combined.metadata) ? combined.metadata : void 0;
308
+ return {
309
+ ...combined,
310
+ id,
311
+ workosId,
312
+ email: typeof combined.email === "string" ? combined.email : void 0,
313
+ name: typeof combined.name === "string" ? combined.name : typeof combined.email === "string" ? combined.email : id,
314
+ organizationId: typeof combined.organizationId === "string" ? combined.organizationId : void 0,
315
+ organizationMembershipId: typeof combined.organizationMembershipId === "string" ? combined.organizationMembershipId : void 0,
316
+ metadata: {
317
+ ...metadata ?? {},
318
+ workosId,
319
+ ...typeof combined.organizationId === "string" ? { organizationId: combined.organizationId } : {},
320
+ ...typeof combined.organizationMembershipId === "string" ? { organizationMembershipId: combined.organizationMembershipId } : {}
321
+ }
322
+ };
323
+ }
324
+ buildUserFromJwtClaims(payload) {
325
+ const userId = this.readJwtClaim(payload, this.jwtClaimOptions?.userId) ?? this.readJwtClaim(payload, "sub");
326
+ const workosId = this.readJwtClaim(payload, this.jwtClaimOptions?.workosId) ?? userId;
327
+ if (!userId || !workosId) {
328
+ return null;
329
+ }
330
+ return {
331
+ id: userId,
332
+ workosId,
333
+ email: this.readJwtClaim(payload, this.jwtClaimOptions?.email) ?? this.readJwtClaim(payload, "email"),
334
+ name: this.readJwtClaim(payload, this.jwtClaimOptions?.name) ?? this.readJwtClaim(payload, "name"),
335
+ organizationId: this.readJwtClaim(payload, this.jwtClaimOptions?.organizationId) ?? this.readJwtClaim(payload, "org_id"),
336
+ organizationMembershipId: this.readJwtClaim(payload, this.jwtClaimOptions?.organizationMembershipId)
337
+ };
338
+ }
339
+ mergeJwtPayloadUser(user, jwtUser, options) {
340
+ if (!jwtUser) {
341
+ return user;
342
+ }
343
+ const trustOrganizationClaims = options?.trustOrganizationClaims ?? true;
344
+ const jwtMetadata = { ...jwtUser.metadata ?? {} };
345
+ if (!trustOrganizationClaims) {
346
+ delete jwtMetadata.organizationId;
347
+ delete jwtMetadata.organizationMembershipId;
348
+ }
349
+ return {
350
+ ...jwtUser,
351
+ ...user,
352
+ organizationId: trustOrganizationClaims ? jwtUser.organizationId ?? user.organizationId : user.organizationId,
353
+ organizationMembershipId: trustOrganizationClaims ? jwtUser.organizationMembershipId ?? user.organizationMembershipId : user.organizationMembershipId,
354
+ memberships: trustOrganizationClaims ? user.memberships ?? jwtUser.memberships : user.memberships,
355
+ metadata: {
356
+ ...jwtMetadata,
357
+ ...user.metadata ?? {}
358
+ }
359
+ };
360
+ }
361
+ readJwtClaim(payload, claimPath) {
362
+ if (!claimPath) {
363
+ return void 0;
364
+ }
365
+ let current = payload;
366
+ for (const segment of claimPath.split(".")) {
367
+ if (!current || typeof current !== "object" || !(segment in current)) {
368
+ return void 0;
369
+ }
370
+ current = current[segment];
371
+ }
372
+ return typeof current === "string" ? current : void 0;
373
+ }
211
374
  // ============================================================================
212
375
  // ISSOProvider Implementation
213
376
  // ============================================================================
@@ -604,6 +767,408 @@ var MastraRBACWorkos = class {
604
767
  return relevantMemberships.map((m) => m.role.slug);
605
768
  }
606
769
  };
770
+ var FILTER_ACCESSIBLE_CHECK_CONCURRENCY = 5;
771
+ var WorkOSFGAResourceNotFoundError = class extends Error {
772
+ status = 404;
773
+ resourceType;
774
+ resourceId;
775
+ constructor(resourceType, resourceId) {
776
+ super(
777
+ `[MastraFGAWorkos] Resource '${resourceType}/${resourceId}' is not registered in WorkOS. Create the '${resourceType}' resource type in your WorkOS dashboard, then register '${resourceId}' using MastraFGAWorkos.createResource() or your seed script. See https://workos.com/docs/fga for setup instructions.`
778
+ );
779
+ this.name = "WorkOSFGAResourceNotFoundError";
780
+ this.resourceType = resourceType;
781
+ this.resourceId = resourceId;
782
+ }
783
+ };
784
+ var WorkOSFGAMembershipResolutionError = class extends Error {
785
+ status = 500;
786
+ userId;
787
+ constructor(user) {
788
+ super(
789
+ "[MastraFGAWorkos] Cannot resolve organization membership for user <redacted>. Ensure fetchMemberships is enabled on MastraAuthWorkos or provide organizationMembershipId on the user."
790
+ );
791
+ this.name = "WorkOSFGAMembershipResolutionError";
792
+ this.userId = user?.id ? "<redacted>" : void 0;
793
+ }
794
+ };
795
+ var MastraFGAWorkos = class {
796
+ workos;
797
+ organizationId;
798
+ resourceMapping;
799
+ permissionMapping;
800
+ constructor(options) {
801
+ const apiKey = options.apiKey ?? process.env.WORKOS_API_KEY;
802
+ const clientId = options.clientId ?? process.env.WORKOS_CLIENT_ID;
803
+ if (!apiKey || !clientId) {
804
+ throw new Error(
805
+ "WorkOS API key and client ID are required. Provide them in the options or set WORKOS_API_KEY and WORKOS_CLIENT_ID environment variables."
806
+ );
807
+ }
808
+ this.workos = new WorkOS(apiKey, { clientId });
809
+ this.organizationId = options.organizationId;
810
+ this.resourceMapping = options.resourceMapping ?? {};
811
+ this.permissionMapping = options.permissionMapping ?? {};
812
+ }
813
+ // ──────────────────────────────────────────────────────────────
814
+ // IFGAProvider — Read-only checks
815
+ // ──────────────────────────────────────────────────────────────
816
+ /**
817
+ * Check if a user has permission on a resource.
818
+ *
819
+ * Resolves the user's organization membership ID, maps the permission
820
+ * via `permissionMapping`, and delegates to `workos.authorization.check()`.
821
+ */
822
+ async check(user, params) {
823
+ const checkOptions = this.buildCheckOptions(user, params);
824
+ if (!checkOptions) return false;
825
+ try {
826
+ const result = await this.workos.authorization.check(checkOptions);
827
+ return result.authorized;
828
+ } catch (error) {
829
+ if (error?.status === 404 || error?.code === "entity_not_found") {
830
+ throw new WorkOSFGAResourceNotFoundError(params.resource.type, params.resource.id);
831
+ }
832
+ throw error;
833
+ }
834
+ }
835
+ /**
836
+ * Require that a user has permission, throwing FGADeniedError if not.
837
+ */
838
+ async require(user, params) {
839
+ const checkOptions = this.buildCheckOptions(user, params, { strictMembershipResolution: true });
840
+ if (!checkOptions) {
841
+ throw new FGADeniedError(user, params.resource, params.permission);
842
+ }
843
+ try {
844
+ const result = await this.workos.authorization.check(checkOptions);
845
+ if (!result.authorized) {
846
+ throw new FGADeniedError(user, params.resource, params.permission);
847
+ }
848
+ } catch (error) {
849
+ if (error instanceof FGADeniedError) throw error;
850
+ if (error?.status === 404 || error?.code === "entity_not_found") {
851
+ throw new WorkOSFGAResourceNotFoundError(params.resource.type, params.resource.id);
852
+ }
853
+ throw error;
854
+ }
855
+ }
856
+ /**
857
+ * Filter resources to only those the user has permission to access.
858
+ *
859
+ * Uses WorkOS `listResourcesForMembership()` when the resource mapping can
860
+ * resolve a parent resource from user context. This avoids one check per
861
+ * resource for list endpoints like agents/workflows/tools.
862
+ *
863
+ * Falls back to per-resource `check()` calls when no parent resource can be
864
+ * resolved from the configured mapping.
865
+ */
866
+ async filterAccessible(user, resources, resourceType, permission) {
867
+ if (resources.length === 0) return [];
868
+ const membershipId = this.resolveOrganizationMembershipId(user);
869
+ if (!membershipId) return [];
870
+ const permissionSlug = this.resolvePermission(permission);
871
+ const parentResource = resourceType === "thread" ? void 0 : this.resolveParentResource(user, resourceType);
872
+ if (parentResource) {
873
+ const accessibleIds = await this.listAccessibleResourceExternalIds({
874
+ organizationMembershipId: membershipId,
875
+ permissionSlug,
876
+ parentResourceExternalId: parentResource.externalId,
877
+ parentResourceTypeSlug: parentResource.typeSlug
878
+ });
879
+ return resources.filter((resource) => {
880
+ const mappedId = this.resolveResourceId(
881
+ user,
882
+ resourceType,
883
+ resource.id,
884
+ "resourceId" in resource && typeof resource.resourceId === "string" ? { resourceId: resource.resourceId } : void 0
885
+ );
886
+ return !!mappedId && accessibleIds.has(mappedId);
887
+ });
888
+ }
889
+ const checks = [];
890
+ for (let start = 0; start < resources.length; start += FILTER_ACCESSIBLE_CHECK_CONCURRENCY) {
891
+ const batch = resources.slice(start, start + FILTER_ACCESSIBLE_CHECK_CONCURRENCY);
892
+ const batchChecks = await Promise.all(
893
+ batch.map(async (resource) => {
894
+ const authorized = await this.check(user, {
895
+ resource: { type: resourceType, id: resource.id },
896
+ permission,
897
+ context: "resourceId" in resource && typeof resource.resourceId === "string" ? { resourceId: resource.resourceId } : void 0
898
+ });
899
+ return { resource, authorized };
900
+ })
901
+ );
902
+ checks.push(...batchChecks);
903
+ }
904
+ return checks.filter((c) => c.authorized).map((c) => c.resource);
905
+ }
906
+ // ──────────────────────────────────────────────────────────────
907
+ // IFGAManager — Write operations
908
+ // ──────────────────────────────────────────────────────────────
909
+ /**
910
+ * Create an authorization resource in WorkOS.
911
+ */
912
+ async createResource(params) {
913
+ const options = {
914
+ externalId: params.externalId,
915
+ name: params.name,
916
+ resourceTypeSlug: params.resourceTypeSlug,
917
+ organizationId: params.organizationId
918
+ };
919
+ if (params.description !== void 0) options.description = params.description;
920
+ if (params.parentResourceId) options.parentResourceId = params.parentResourceId;
921
+ if (params.parentResourceExternalId) {
922
+ options.parentResourceExternalId = params.parentResourceExternalId;
923
+ options.parentResourceTypeSlug = params.parentResourceTypeSlug;
924
+ }
925
+ const result = await this.workos.authorization.createResource(options);
926
+ return this.mapAuthorizationResource(result);
927
+ }
928
+ /**
929
+ * Get an authorization resource by ID.
930
+ */
931
+ async getResource(resourceId) {
932
+ const result = await this.workos.authorization.getResource(resourceId);
933
+ return this.mapAuthorizationResource(result);
934
+ }
935
+ /**
936
+ * List authorization resources with optional filters.
937
+ */
938
+ async listResources(options) {
939
+ const listOptions = {};
940
+ if (options?.organizationId) listOptions.organizationId = options.organizationId;
941
+ if (options?.resourceTypeSlug) listOptions.resourceTypeSlug = options.resourceTypeSlug;
942
+ if (options?.parentResourceId) listOptions.parentResourceId = options.parentResourceId;
943
+ if (options?.search) listOptions.search = options.search;
944
+ if (options?.limit) listOptions.limit = options.limit;
945
+ if (options?.after) listOptions.after = options.after;
946
+ const result = await this.workos.authorization.listResources(listOptions);
947
+ return result.data.map((r) => this.mapAuthorizationResource(r));
948
+ }
949
+ /**
950
+ * Update an authorization resource.
951
+ */
952
+ async updateResource(params) {
953
+ const options = { resourceId: params.resourceId };
954
+ if (params.name !== void 0) options.name = params.name;
955
+ if (params.description !== void 0) options.description = params.description;
956
+ const result = await this.workos.authorization.updateResource(options);
957
+ return this.mapAuthorizationResource(result);
958
+ }
959
+ /**
960
+ * Delete an authorization resource.
961
+ */
962
+ async deleteResource(params) {
963
+ if ("resourceId" in params && params.resourceId) {
964
+ await this.workos.authorization.deleteResource({ resourceId: params.resourceId });
965
+ } else if ("externalId" in params && params.externalId && params.resourceTypeSlug) {
966
+ await this.workos.authorization.deleteResourceByExternalId({
967
+ externalId: params.externalId,
968
+ resourceTypeSlug: params.resourceTypeSlug,
969
+ organizationId: params.organizationId
970
+ });
971
+ }
972
+ }
973
+ /**
974
+ * Assign a role to an organization membership on a resource.
975
+ */
976
+ async assignRole(params) {
977
+ const options = {
978
+ organizationMembershipId: params.organizationMembershipId,
979
+ roleSlug: params.roleSlug
980
+ };
981
+ if (params.resourceId) options.resourceId = params.resourceId;
982
+ if (params.resourceExternalId) {
983
+ options.resourceExternalId = params.resourceExternalId;
984
+ options.resourceTypeSlug = params.resourceTypeSlug;
985
+ }
986
+ const result = await this.workos.authorization.assignRole(options);
987
+ return {
988
+ id: result.id,
989
+ role: result.role,
990
+ resource: {
991
+ id: result.resource.id,
992
+ externalId: result.resource.externalId,
993
+ resourceTypeSlug: result.resource.resourceTypeSlug
994
+ }
995
+ };
996
+ }
997
+ /**
998
+ * Remove a role assignment.
999
+ */
1000
+ async removeRole(params) {
1001
+ const options = {
1002
+ organizationMembershipId: params.organizationMembershipId,
1003
+ roleSlug: params.roleSlug
1004
+ };
1005
+ if (params.resourceId) options.resourceId = params.resourceId;
1006
+ if (params.resourceExternalId) {
1007
+ options.resourceExternalId = params.resourceExternalId;
1008
+ options.resourceTypeSlug = params.resourceTypeSlug;
1009
+ }
1010
+ await this.workos.authorization.removeRole(options);
1011
+ }
1012
+ /**
1013
+ * List role assignments for an organization membership.
1014
+ */
1015
+ async listRoleAssignments(options) {
1016
+ const result = await this.workos.authorization.listRoleAssignments({
1017
+ organizationMembershipId: options.organizationMembershipId,
1018
+ ...options.limit && { limit: options.limit },
1019
+ ...options.after && { after: options.after }
1020
+ });
1021
+ return result.data.map((ra) => ({
1022
+ id: ra.id,
1023
+ role: ra.role,
1024
+ resource: {
1025
+ id: ra.resource.id,
1026
+ externalId: ra.resource.externalId,
1027
+ resourceTypeSlug: ra.resource.resourceTypeSlug
1028
+ }
1029
+ }));
1030
+ }
1031
+ // ──────────────────────────────────────────────────────────────
1032
+ // Internal helpers
1033
+ // ──────────────────────────────────────────────────────────────
1034
+ /**
1035
+ * Resolve the organization membership ID from a user object.
1036
+ * Looks for organizationMembershipId, then finds membership matching
1037
+ * configured organizationId, then falls back to first membership.
1038
+ *
1039
+ * Returns undefined if no membership can be resolved, which causes
1040
+ * authorization checks to deny access. Enable `fetchMemberships: true`
1041
+ * on MastraAuthWorkos to populate the memberships field.
1042
+ */
1043
+ resolveOrganizationMembershipId(user, options) {
1044
+ if (user?.organizationMembershipId) return user.organizationMembershipId;
1045
+ if (!user?.memberships?.length) {
1046
+ console.warn(
1047
+ "[MastraFGAWorkos] Cannot resolve organization membership for user <redacted>. Ensure fetchMemberships is enabled on MastraAuthWorkos when using FGA."
1048
+ );
1049
+ if (options?.strictMembershipResolution) {
1050
+ throw new WorkOSFGAMembershipResolutionError(user);
1051
+ }
1052
+ return void 0;
1053
+ }
1054
+ if (this.organizationId) {
1055
+ const match = user.memberships.find((m) => m.organizationId === this.organizationId);
1056
+ if (match) return match.id;
1057
+ console.warn("[MastraFGAWorkos] User <redacted> does not belong to configured organization <redacted>.");
1058
+ if (options?.strictMembershipResolution) {
1059
+ throw new WorkOSFGAMembershipResolutionError(user);
1060
+ }
1061
+ return void 0;
1062
+ }
1063
+ return user.memberships[0].id;
1064
+ }
1065
+ /**
1066
+ * Map a Mastra permission string to a WorkOS permission slug via permissionMapping.
1067
+ * Falls back to the original permission if no mapping is found.
1068
+ */
1069
+ resolvePermission(permission) {
1070
+ return this.permissionMapping[permission] ?? permission;
1071
+ }
1072
+ /**
1073
+ * Resolve the parent resource context needed for WorkOS resource discovery.
1074
+ */
1075
+ resolveParentResource(user, resourceType) {
1076
+ const mapping = this.getResourceMapping(resourceType);
1077
+ const externalId = mapping?.deriveId?.({ user });
1078
+ const parentTypeSlug = mapping?.parentFgaResourceType ?? mapping?.parentResourceTypeSlug;
1079
+ if (!mapping?.fgaResourceType || !externalId || !parentTypeSlug || parentTypeSlug === mapping.fgaResourceType) {
1080
+ return void 0;
1081
+ }
1082
+ return {
1083
+ externalId,
1084
+ typeSlug: parentTypeSlug
1085
+ };
1086
+ }
1087
+ /**
1088
+ * Resolve the FGA resource ID using resourceMapping's deriveId function.
1089
+ * Falls back to the original resource ID if no mapping is found.
1090
+ */
1091
+ resolveResourceId(user, resourceType, resourceId, context) {
1092
+ const mapping = this.getResourceMapping(resourceType);
1093
+ const derivedId = mapping?.deriveId?.({
1094
+ user,
1095
+ resourceId: context?.resourceId ?? resourceId,
1096
+ requestContext: context?.requestContext
1097
+ });
1098
+ return derivedId ?? resourceId;
1099
+ }
1100
+ buildCheckOptions(user, params, options) {
1101
+ const membershipId = this.resolveOrganizationMembershipId(user, options);
1102
+ if (!membershipId) return null;
1103
+ const permissionSlug = this.resolvePermission(params.permission);
1104
+ const resourceId = this.resolveResourceId(user, params.resource.type, params.resource.id, params.context);
1105
+ const checkOptions = {
1106
+ organizationMembershipId: membershipId,
1107
+ permissionSlug
1108
+ };
1109
+ if (!resourceId) {
1110
+ return checkOptions;
1111
+ }
1112
+ const mapping = this.getResourceMapping(params.resource.type);
1113
+ if (mapping) {
1114
+ checkOptions.resourceExternalId = resourceId;
1115
+ checkOptions.resourceTypeSlug = mapping.fgaResourceType;
1116
+ } else {
1117
+ checkOptions.resourceExternalId = params.resource.id;
1118
+ checkOptions.resourceTypeSlug = params.resource.type;
1119
+ }
1120
+ return checkOptions;
1121
+ }
1122
+ getResourceMapping(resourceType) {
1123
+ const aliases = resourceType === "agent" ? ["agent", "agents"] : resourceType === "workflow" ? ["workflow", "workflows"] : resourceType === "tool" ? ["tool", "tools"] : resourceType === "thread" ? ["thread", "threads", "memory"] : [resourceType];
1124
+ for (const key of aliases) {
1125
+ const mapping = this.resourceMapping[key];
1126
+ if (mapping) {
1127
+ return mapping;
1128
+ }
1129
+ }
1130
+ return void 0;
1131
+ }
1132
+ /**
1133
+ * List accessible child resources for a membership, following pagination.
1134
+ */
1135
+ async listAccessibleResourceExternalIds(params) {
1136
+ const accessibleIds = /* @__PURE__ */ new Set();
1137
+ let after;
1138
+ do {
1139
+ const result = await this.workos.authorization.listResourcesForMembership({
1140
+ organizationMembershipId: params.organizationMembershipId,
1141
+ permissionSlug: params.permissionSlug,
1142
+ parentResourceExternalId: params.parentResourceExternalId,
1143
+ parentResourceTypeSlug: params.parentResourceTypeSlug,
1144
+ ...after ? { after } : {},
1145
+ limit: 100,
1146
+ order: "asc"
1147
+ });
1148
+ for (const resource of result.data ?? []) {
1149
+ if (typeof resource?.externalId === "string") {
1150
+ accessibleIds.add(resource.externalId);
1151
+ }
1152
+ }
1153
+ after = result.listMetadata?.after ?? void 0;
1154
+ } while (after);
1155
+ return accessibleIds;
1156
+ }
1157
+ /**
1158
+ * Map a WorkOS AuthorizationResource to Mastra's FGAResource type.
1159
+ */
1160
+ mapAuthorizationResource(resource) {
1161
+ return {
1162
+ id: resource.id,
1163
+ externalId: resource.externalId,
1164
+ name: resource.name,
1165
+ description: resource.description,
1166
+ resourceTypeSlug: resource.resourceTypeSlug,
1167
+ organizationId: resource.organizationId,
1168
+ parentResourceId: resource.parentResourceId
1169
+ };
1170
+ }
1171
+ };
607
1172
 
608
1173
  // src/directory-sync.ts
609
1174
  var WorkOSDirectorySync = class {
@@ -870,7 +1435,15 @@ var WorkOSAdminPortal = class {
870
1435
  return result.link;
871
1436
  }
872
1437
  };
1438
+ /**
1439
+ * WorkOS FGA provider for Mastra.
1440
+ *
1441
+ * Integrates WorkOS Authorization API with Mastra's FGA interface
1442
+ * for permission-based, resource-level authorization.
1443
+ *
1444
+ * @license Mastra Enterprise License - see ee/LICENSE
1445
+ */
873
1446
 
874
- export { MastraAuthWorkos, MastraRBACWorkos, WebSessionStorage, WorkOSAdminPortal, WorkOSDirectorySync, mapWorkOSUserToEEUser };
1447
+ export { MastraAuthWorkos, MastraFGAWorkos, MastraRBACWorkos, WebSessionStorage, WorkOSAdminPortal, WorkOSDirectorySync, WorkOSFGAMembershipResolutionError, WorkOSFGAResourceNotFoundError, mapWorkOSUserToEEUser };
875
1448
  //# sourceMappingURL=index.js.map
876
1449
  //# sourceMappingURL=index.js.map