@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/CHANGELOG.md +50 -0
- package/README.md +70 -6
- package/dist/auth-provider.d.ts +14 -0
- package/dist/auth-provider.d.ts.map +1 -1
- package/dist/fga-provider.d.ts +158 -0
- package/dist/fga-provider.d.ts.map +1 -0
- package/dist/index.cjs +595 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +593 -20
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +135 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +11 -11
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
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
214
|
+
let memberships;
|
|
215
|
+
if (this.fetchMemberships) {
|
|
170
216
|
try {
|
|
171
|
-
|
|
172
|
-
|
|
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
|