@mastra/auth-google 0.0.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 ADDED
@@ -0,0 +1,686 @@
1
+ import { MastraAuthProvider } from '@mastra/core/server';
2
+ import { createRemoteJWKSet, jwtVerify } from 'jose';
3
+ import { createSign } from 'crypto';
4
+ import { resolvePermissionsFromMapping, matchesPermission } from '@mastra/core/auth/ee';
5
+ import { LRUCache } from 'lru-cache';
6
+
7
+ // src/auth-provider.ts
8
+
9
+ // src/types.ts
10
+ function mapGoogleClaimsToUser(payload) {
11
+ const googleId = payload.sub || "";
12
+ const email = payload.email;
13
+ const hostedDomain = payload.hd;
14
+ const emailVerified = payload.email_verified;
15
+ return {
16
+ id: googleId,
17
+ googleId,
18
+ email,
19
+ name: payload.name || [payload.given_name, payload.family_name].filter(Boolean).join(" ") || email || void 0,
20
+ avatarUrl: payload.picture,
21
+ expiresAt: typeof payload.exp === "number" ? new Date(payload.exp * 1e3) : void 0,
22
+ hostedDomain,
23
+ emailVerified,
24
+ groups: payload.groups,
25
+ metadata: {
26
+ googleId,
27
+ hostedDomain,
28
+ emailVerified,
29
+ givenName: payload.given_name,
30
+ familyName: payload.family_name
31
+ }
32
+ };
33
+ }
34
+
35
+ // src/auth-provider.ts
36
+ var GOOGLE_AUTHORIZATION_URL = "https://accounts.google.com/o/oauth2/v2/auth";
37
+ var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
38
+ var GOOGLE_JWKS_URL = "https://www.googleapis.com/oauth2/v3/certs";
39
+ var GOOGLE_ISSUERS = ["https://accounts.google.com", "accounts.google.com"];
40
+ var DEFAULT_COOKIE_NAME = "google_session";
41
+ var DEFAULT_COOKIE_MAX_AGE = 86400;
42
+ var DEFAULT_SCOPES = ["openid", "profile", "email"];
43
+ var STATE_TOKEN_EXPIRY_MS = 10 * 60 * 1e3;
44
+ var SALT_LENGTH = 16;
45
+ var IV_LENGTH = 12;
46
+ function getRequestHeader(request, name) {
47
+ if (request instanceof Request) {
48
+ return request.headers.get(name);
49
+ }
50
+ return request.raw?.headers.get(name) ?? request.headers?.get(name) ?? request.header(name) ?? null;
51
+ }
52
+ function normalizeDomain(domain) {
53
+ const normalized = domain?.trim().toLowerCase().replace(/^@/, "");
54
+ return normalized || void 0;
55
+ }
56
+ function normalizeAllowedDomains(value) {
57
+ if (!value) return [];
58
+ const parts = Array.isArray(value) ? value : value.split(",");
59
+ return Array.from(new Set(parts.map(normalizeDomain).filter((domain) => !!domain)));
60
+ }
61
+ function escapeRegex(str) {
62
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
63
+ }
64
+ function getServerRedirectStateSuffix(state) {
65
+ const separatorIndex = state.indexOf("|");
66
+ return separatorIndex === -1 ? "" : state.slice(separatorIndex);
67
+ }
68
+ function getStateTokenFromCallbackState(state) {
69
+ const separatorIndex = state.indexOf("|");
70
+ return separatorIndex === -1 ? state : state.slice(0, separatorIndex);
71
+ }
72
+ function verifyCallbackStateSuffix(callbackState, originalState) {
73
+ const callbackSuffix = getServerRedirectStateSuffix(callbackState);
74
+ if (!callbackSuffix) return;
75
+ if (callbackSuffix !== getServerRedirectStateSuffix(originalState)) {
76
+ throw new Error("Invalid state redirect suffix");
77
+ }
78
+ }
79
+ function getExpirationMs(expiresAt) {
80
+ if (expiresAt === void 0 || expiresAt === null) {
81
+ return void 0;
82
+ }
83
+ if (expiresAt instanceof Date) {
84
+ return expiresAt.getTime();
85
+ }
86
+ if (typeof expiresAt === "string" || typeof expiresAt === "number") {
87
+ return new Date(expiresAt).getTime();
88
+ }
89
+ return Number.NaN;
90
+ }
91
+ async function deriveKey(password, salt, usage) {
92
+ const encoder = new TextEncoder();
93
+ const keyMaterial = await crypto.subtle.importKey("raw", encoder.encode(password), "PBKDF2", false, [
94
+ "deriveBits",
95
+ "deriveKey"
96
+ ]);
97
+ return crypto.subtle.deriveKey(
98
+ { name: "PBKDF2", salt, iterations: 1e5, hash: "SHA-256" },
99
+ keyMaterial,
100
+ { name: "AES-GCM", length: 256 },
101
+ false,
102
+ [usage]
103
+ );
104
+ }
105
+ async function encryptSession(data, password) {
106
+ const encoder = new TextEncoder();
107
+ const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
108
+ const key = await deriveKey(password, salt, "encrypt");
109
+ const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
110
+ const encrypted = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, encoder.encode(JSON.stringify(data)));
111
+ const combined = new Uint8Array(salt.length + iv.length + new Uint8Array(encrypted).length);
112
+ combined.set(salt);
113
+ combined.set(iv, salt.length);
114
+ combined.set(new Uint8Array(encrypted), salt.length + iv.length);
115
+ return btoa(String.fromCharCode(...combined));
116
+ }
117
+ async function decryptSession(encrypted, password) {
118
+ const combined = Uint8Array.from(atob(encrypted), (c) => c.charCodeAt(0));
119
+ if (combined.length < SALT_LENGTH + IV_LENGTH + 1) {
120
+ throw new Error("Invalid encrypted session data");
121
+ }
122
+ const salt = combined.slice(0, SALT_LENGTH);
123
+ const iv = combined.slice(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);
124
+ const data = combined.slice(SALT_LENGTH + IV_LENGTH);
125
+ const key = await deriveKey(password, salt, "decrypt");
126
+ const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, data);
127
+ return JSON.parse(new TextDecoder().decode(decrypted));
128
+ }
129
+ async function hmacSign(data, secret) {
130
+ const encoder = new TextEncoder();
131
+ const cryptoKey = await crypto.subtle.importKey(
132
+ "raw",
133
+ encoder.encode(secret),
134
+ { name: "HMAC", hash: "SHA-256" },
135
+ false,
136
+ ["sign"]
137
+ );
138
+ const signature = await crypto.subtle.sign("HMAC", cryptoKey, encoder.encode(data));
139
+ const sigBytes = new Uint8Array(signature);
140
+ return btoa(String.fromCharCode(...sigBytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
141
+ }
142
+ function timingSafeEqual(a, b) {
143
+ if (a.length !== b.length) return false;
144
+ let result = 0;
145
+ for (let i = 0; i < a.length; i++) {
146
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
147
+ }
148
+ return result === 0;
149
+ }
150
+ async function createStateToken(originalState, redirectUri, nonce, secret) {
151
+ const payload = {
152
+ s: originalState,
153
+ r: redirectUri,
154
+ e: Date.now() + STATE_TOKEN_EXPIRY_MS,
155
+ n: nonce
156
+ };
157
+ const payloadB64 = btoa(JSON.stringify(payload));
158
+ const signature = await hmacSign(payloadB64, secret);
159
+ return `${payloadB64}.${signature}`;
160
+ }
161
+ async function verifyStateToken(stateToken, secret) {
162
+ const parts = stateToken.split(".");
163
+ if (parts.length !== 2) {
164
+ throw new Error("Invalid state token format");
165
+ }
166
+ const [payloadB64, signature] = parts;
167
+ const expectedSig = await hmacSign(payloadB64, secret);
168
+ if (!timingSafeEqual(signature, expectedSig)) {
169
+ throw new Error("Invalid state token signature");
170
+ }
171
+ let payload;
172
+ try {
173
+ payload = JSON.parse(atob(payloadB64));
174
+ } catch {
175
+ throw new Error("Invalid state token payload");
176
+ }
177
+ if (payload.e < Date.now()) {
178
+ throw new Error("State token has expired");
179
+ }
180
+ return {
181
+ originalState: payload.s,
182
+ redirectUri: payload.r,
183
+ nonce: payload.n
184
+ };
185
+ }
186
+ function hasExpired(payload) {
187
+ return typeof payload.exp === "number" && payload.exp * 1e3 < Date.now();
188
+ }
189
+ var MastraAuthGoogle = class extends MastraAuthProvider {
190
+ clientId;
191
+ clientSecret;
192
+ redirectUri;
193
+ scopes;
194
+ cookieName;
195
+ cookieMaxAge;
196
+ cookiePassword;
197
+ secureCookies;
198
+ allowedDomains;
199
+ hostedDomain;
200
+ ssoEnabled;
201
+ jwks;
202
+ constructor(options) {
203
+ super({ name: options?.name ?? "google" });
204
+ const clientId = options?.clientId ?? process.env.GOOGLE_CLIENT_ID;
205
+ if (!clientId) {
206
+ throw new Error(
207
+ "Google client ID is required. Provide it in the options or set GOOGLE_CLIENT_ID environment variable."
208
+ );
209
+ }
210
+ const allowedDomains = normalizeAllowedDomains(options?.allowedDomains ?? process.env.GOOGLE_ALLOWED_DOMAINS);
211
+ const configuredHostedDomain = normalizeDomain(options?.hostedDomain ?? process.env.GOOGLE_HOSTED_DOMAIN);
212
+ const clientSecret = options?.clientSecret ?? process.env.GOOGLE_CLIENT_SECRET;
213
+ const redirectUri = options?.redirectUri ?? process.env.GOOGLE_REDIRECT_URI;
214
+ const hasConfiguredCookiePassword = !!(options?.session?.cookiePassword ?? process.env.GOOGLE_COOKIE_PASSWORD);
215
+ const cookiePassword = options?.session?.cookiePassword ?? process.env.GOOGLE_COOKIE_PASSWORD ?? crypto.randomUUID() + crypto.randomUUID();
216
+ this.clientId = clientId;
217
+ this.clientSecret = clientSecret ?? null;
218
+ this.redirectUri = redirectUri ?? null;
219
+ this.scopes = options?.scopes ?? DEFAULT_SCOPES;
220
+ this.cookieName = options?.session?.cookieName ?? DEFAULT_COOKIE_NAME;
221
+ this.cookieMaxAge = options?.session?.cookieMaxAge ?? DEFAULT_COOKIE_MAX_AGE;
222
+ this.cookiePassword = cookiePassword;
223
+ this.secureCookies = options?.session?.secureCookies ?? process.env.NODE_ENV === "production";
224
+ this.allowedDomains = allowedDomains;
225
+ this.hostedDomain = configuredHostedDomain ?? (allowedDomains.length === 1 ? allowedDomains[0] : void 0);
226
+ this.ssoEnabled = !!clientSecret;
227
+ this.jwks = createRemoteJWKSet(new URL(GOOGLE_JWKS_URL));
228
+ if (this.ssoEnabled) {
229
+ if (cookiePassword.length < 32) {
230
+ throw new Error(
231
+ "Cookie password must be at least 32 characters for SSO. Set GOOGLE_COOKIE_PASSWORD environment variable."
232
+ );
233
+ }
234
+ if (!hasConfiguredCookiePassword) {
235
+ const message = "[MastraAuthGoogle] GOOGLE_COOKIE_PASSWORD is required for Google SSO in production. Set GOOGLE_COOKIE_PASSWORD or pass session.cookiePassword.";
236
+ if (process.env.NODE_ENV === "production") {
237
+ throw new Error(message);
238
+ }
239
+ console.warn(
240
+ `${message} Using an auto-generated value for development only; sessions will not survive restarts.`
241
+ );
242
+ }
243
+ this.attachSSOProvider();
244
+ this.attachSessionProvider();
245
+ }
246
+ this.registerOptions(options);
247
+ }
248
+ async authenticateToken(token, request) {
249
+ if (this.ssoEnabled && request) {
250
+ const sessionUser = await this.getUserFromSessionCookie(request);
251
+ if (sessionUser) return sessionUser;
252
+ }
253
+ if (!token || typeof token !== "string") {
254
+ return null;
255
+ }
256
+ try {
257
+ const user = await this.verifyIdToken(token);
258
+ return user;
259
+ } catch {
260
+ return null;
261
+ }
262
+ }
263
+ authorizeUser(user) {
264
+ if (!user?.googleId && !user?.id) return false;
265
+ const expiresAt = getExpirationMs(user.expiresAt);
266
+ if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt < Date.now())) return false;
267
+ return this.isHostedDomainAllowed(user.hostedDomain);
268
+ }
269
+ async getCurrentUser(request) {
270
+ if (this.ssoEnabled) {
271
+ const sessionUser = await this.getUserFromSessionCookie(request);
272
+ if (sessionUser) return sessionUser;
273
+ }
274
+ const token = this.extractBearerToken(request);
275
+ if (!token) return null;
276
+ return this.authenticateToken(token, request);
277
+ }
278
+ async getUser(_userId) {
279
+ return null;
280
+ }
281
+ getUserProfileUrl(user) {
282
+ return `/user/${user.id}`;
283
+ }
284
+ isSSOEnabled() {
285
+ return this.ssoEnabled;
286
+ }
287
+ getAllowedDomains() {
288
+ return [...this.allowedDomains];
289
+ }
290
+ getHostedDomain() {
291
+ return this.hostedDomain;
292
+ }
293
+ getClientId() {
294
+ return this.clientId;
295
+ }
296
+ async verifyIdToken(token, nonce) {
297
+ const { payload } = await jwtVerify(token, this.jwks, {
298
+ issuer: GOOGLE_ISSUERS,
299
+ audience: this.clientId
300
+ });
301
+ if (nonce && payload.nonce !== nonce) {
302
+ throw new Error("Invalid Google ID token nonce");
303
+ }
304
+ if (hasExpired(payload)) {
305
+ throw new Error("Google ID token has expired");
306
+ }
307
+ const user = mapGoogleClaimsToUser(payload);
308
+ if (!user.googleId) {
309
+ throw new Error("Google ID token is missing subject");
310
+ }
311
+ if (!this.isHostedDomainAllowed(user.hostedDomain)) {
312
+ throw new Error("Google user is not in an allowed hosted domain");
313
+ }
314
+ return user;
315
+ }
316
+ isHostedDomainAllowed(hostedDomain) {
317
+ if (this.allowedDomains.length === 0) return true;
318
+ const domain = normalizeDomain(hostedDomain);
319
+ if (!domain) return false;
320
+ return this.allowedDomains.includes(domain);
321
+ }
322
+ extractBearerToken(request) {
323
+ const authHeader = request.headers.get("Authorization");
324
+ if (!authHeader) return null;
325
+ const token = authHeader.replace(/^Bearer\s+/i, "").trim();
326
+ return token || null;
327
+ }
328
+ cookieFlags(maxAge) {
329
+ const flags = `Path=/; HttpOnly; SameSite=Lax; Max-Age=${maxAge}`;
330
+ return this.secureCookies ? `${flags}; Secure` : flags;
331
+ }
332
+ async getUserFromSessionCookie(request) {
333
+ const cookie = getRequestHeader(request, "cookie");
334
+ if (!cookie) return null;
335
+ const match = cookie.match(new RegExp(`(?:^|;\\s*)${escapeRegex(this.cookieName)}=([^;]+)`));
336
+ if (!match?.[1]) return null;
337
+ try {
338
+ const sessionData = await decryptSession(decodeURIComponent(match[1]), this.cookiePassword);
339
+ if (sessionData.expiresAt < Date.now()) {
340
+ return null;
341
+ }
342
+ const userExpiresAt = getExpirationMs(sessionData.user.expiresAt);
343
+ if (userExpiresAt !== void 0 && (!Number.isFinite(userExpiresAt) || userExpiresAt < Date.now())) {
344
+ return null;
345
+ }
346
+ const { expiresAt: _expiresAt, ...sessionUser } = sessionData.user;
347
+ const user = {
348
+ ...sessionUser,
349
+ ...userExpiresAt !== void 0 ? { expiresAt: new Date(userExpiresAt) } : {}
350
+ };
351
+ if (!this.isHostedDomainAllowed(user.hostedDomain)) {
352
+ return null;
353
+ }
354
+ return user;
355
+ } catch {
356
+ return null;
357
+ }
358
+ }
359
+ attachSSOProvider() {
360
+ const self = this;
361
+ this.getLoginUrl = async function(redirectUri, state) {
362
+ const actualRedirectUri = redirectUri ?? self.redirectUri;
363
+ if (!actualRedirectUri) {
364
+ throw new Error("Redirect URI is required for Google SSO. Set GOOGLE_REDIRECT_URI or pass redirectUri.");
365
+ }
366
+ const nonce = crypto.randomUUID();
367
+ const signedState = await createStateToken(state, actualRedirectUri, nonce, self.cookiePassword);
368
+ const oauthState = `${signedState}${getServerRedirectStateSuffix(state)}`;
369
+ const params = new URLSearchParams({
370
+ client_id: self.clientId,
371
+ response_type: "code",
372
+ scope: self.scopes.join(" "),
373
+ redirect_uri: actualRedirectUri,
374
+ state: oauthState,
375
+ nonce
376
+ });
377
+ if (self.hostedDomain) {
378
+ params.set("hd", self.hostedDomain);
379
+ }
380
+ return `${GOOGLE_AUTHORIZATION_URL}?${params.toString()}`;
381
+ };
382
+ this.handleCallback = async function(code, callbackState) {
383
+ const signedState = getStateTokenFromCallbackState(callbackState);
384
+ const { originalState, redirectUri, nonce } = await verifyStateToken(signedState, self.cookiePassword);
385
+ verifyCallbackStateSuffix(callbackState, originalState);
386
+ const tokenResponse = await fetch(GOOGLE_TOKEN_URL, {
387
+ method: "POST",
388
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
389
+ body: new URLSearchParams({
390
+ grant_type: "authorization_code",
391
+ code,
392
+ client_id: self.clientId,
393
+ client_secret: self.clientSecret,
394
+ redirect_uri: redirectUri
395
+ }),
396
+ signal: AbortSignal.timeout(1e4)
397
+ });
398
+ if (!tokenResponse.ok) {
399
+ const error = await tokenResponse.text();
400
+ throw new Error(`Google token exchange failed: ${error}`);
401
+ }
402
+ const tokens = await tokenResponse.json();
403
+ if (!tokens.id_token) {
404
+ throw new Error("Google token response did not include an ID token");
405
+ }
406
+ const user = await self.verifyIdToken(tokens.id_token, nonce);
407
+ const sessionData = {
408
+ user,
409
+ expiresAt: Date.now() + self.cookieMaxAge * 1e3
410
+ };
411
+ const encryptedSession = await encryptSession(sessionData, self.cookiePassword);
412
+ const cookieValue = `${self.cookieName}=${encodeURIComponent(encryptedSession)}; ${self.cookieFlags(self.cookieMaxAge)}`;
413
+ return {
414
+ user,
415
+ tokens: {
416
+ accessToken: tokens.access_token,
417
+ refreshToken: tokens.refresh_token,
418
+ idToken: tokens.id_token,
419
+ expiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
420
+ },
421
+ cookies: [cookieValue]
422
+ };
423
+ };
424
+ this.getLoginButtonConfig = function() {
425
+ return {
426
+ provider: "google",
427
+ text: "Sign in with Google",
428
+ description: "Sign in using your Google account"
429
+ };
430
+ };
431
+ this.getLoginCookies = function() {
432
+ return [];
433
+ };
434
+ this.getLogoutUrl = async function() {
435
+ return null;
436
+ };
437
+ }
438
+ attachSessionProvider() {
439
+ const self = this;
440
+ this.createSession = async function(userId, metadata) {
441
+ const now = /* @__PURE__ */ new Date();
442
+ return {
443
+ id: crypto.randomUUID(),
444
+ userId,
445
+ createdAt: now,
446
+ expiresAt: new Date(now.getTime() + self.cookieMaxAge * 1e3),
447
+ metadata
448
+ };
449
+ };
450
+ this.validateSession = async function() {
451
+ return null;
452
+ };
453
+ this.destroySession = async function() {
454
+ };
455
+ this.refreshSession = async function() {
456
+ return null;
457
+ };
458
+ this.getSessionIdFromRequest = function(request) {
459
+ const cookie = request.headers.get("Cookie");
460
+ if (!cookie) return null;
461
+ const match = cookie.match(new RegExp(`(?:^|;\\s*)${escapeRegex(self.cookieName)}=([^;]+)`));
462
+ return match?.[1] ? decodeURIComponent(match[1]) : null;
463
+ };
464
+ this.getSessionHeaders = function() {
465
+ return {};
466
+ };
467
+ this.getClearSessionHeaders = function() {
468
+ return {
469
+ "Set-Cookie": `${self.cookieName}=; ${self.cookieFlags(0)}`
470
+ };
471
+ };
472
+ }
473
+ };
474
+ var OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";
475
+ var DIRECTORY_GROUPS_URL = "https://admin.googleapis.com/admin/directory/v1/groups";
476
+ var DEFAULT_DIRECTORY_SCOPES = ["https://www.googleapis.com/auth/admin.directory.group.readonly"];
477
+ var DEFAULT_CACHE_TTL_MS = 60 * 1e3;
478
+ var DEFAULT_CACHE_MAX_SIZE = 1e3;
479
+ var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
480
+ var MastraRBACGoogle = class {
481
+ options;
482
+ rolesCache;
483
+ accessToken;
484
+ tokenExpiresAt = 0;
485
+ tokenRefreshPromise;
486
+ get roleMapping() {
487
+ return this.options.roleMapping;
488
+ }
489
+ constructor(options) {
490
+ if (!options.roleMapping) {
491
+ throw new Error("Google RBAC roleMapping is required.");
492
+ }
493
+ this.options = options;
494
+ this.accessToken = options.accessToken;
495
+ this.rolesCache = new LRUCache({
496
+ max: options.cache?.maxSize ?? DEFAULT_CACHE_MAX_SIZE,
497
+ ttl: options.cache?.ttlMs ?? DEFAULT_CACHE_TTL_MS
498
+ });
499
+ }
500
+ async getRoles(user) {
501
+ if (Array.isArray(user.groups)) {
502
+ return user.groups;
503
+ }
504
+ const userKey = this.resolveUserKey(user);
505
+ if (!userKey) {
506
+ return [];
507
+ }
508
+ const cached = this.rolesCache.get(userKey);
509
+ if (cached) {
510
+ return cached;
511
+ }
512
+ const rolesPromise = this.fetchRolesFromGoogle(userKey).catch((err) => {
513
+ console.error("[MastraRBACGoogle] Failed to fetch Google Workspace groups:", err);
514
+ this.rolesCache.delete(userKey);
515
+ throw err;
516
+ });
517
+ this.rolesCache.set(userKey, rolesPromise);
518
+ return rolesPromise;
519
+ }
520
+ async hasRole(user, role) {
521
+ const roles = await this.getRoles(user);
522
+ return roles.includes(role);
523
+ }
524
+ async getPermissions(user) {
525
+ const roles = await this.getRoles(user);
526
+ if (roles.length === 0) {
527
+ return this.options.roleMapping["_default"] ?? [];
528
+ }
529
+ return resolvePermissionsFromMapping(roles, this.options.roleMapping);
530
+ }
531
+ async hasPermission(user, permission) {
532
+ const permissions = await this.getPermissions(user);
533
+ return permissions.some((granted) => matchesPermission(granted, permission));
534
+ }
535
+ async hasAllPermissions(user, permissions) {
536
+ const userPermissions = await this.getPermissions(user);
537
+ return permissions.every((required) => userPermissions.some((granted) => matchesPermission(granted, required)));
538
+ }
539
+ async hasAnyPermission(user, permissions) {
540
+ const userPermissions = await this.getPermissions(user);
541
+ return permissions.some((required) => userPermissions.some((granted) => matchesPermission(granted, required)));
542
+ }
543
+ async getAvailableRoles() {
544
+ return Object.keys(this.options.roleMapping).filter((key) => key !== "_default").map((key) => ({ id: key, name: key }));
545
+ }
546
+ async getPermissionsForRole(roleId) {
547
+ return resolvePermissionsFromMapping([roleId], this.options.roleMapping);
548
+ }
549
+ clearCache() {
550
+ this.rolesCache.clear();
551
+ }
552
+ clearUserCache(userKey) {
553
+ this.rolesCache.delete(userKey);
554
+ }
555
+ getCacheStats() {
556
+ return {
557
+ size: this.rolesCache.size,
558
+ maxSize: this.rolesCache.max
559
+ };
560
+ }
561
+ resolveUserKey(user) {
562
+ if (this.options.getUserKey) {
563
+ return this.options.getUserKey(user);
564
+ }
565
+ return user.email;
566
+ }
567
+ async fetchRolesFromGoogle(userKey) {
568
+ const token = await this.getToken();
569
+ const roles = /* @__PURE__ */ new Set();
570
+ let pageToken;
571
+ do {
572
+ const url = new URL(DIRECTORY_GROUPS_URL);
573
+ url.searchParams.set("userKey", userKey);
574
+ url.searchParams.set("maxResults", "200");
575
+ if (pageToken) {
576
+ url.searchParams.set("pageToken", pageToken);
577
+ }
578
+ const response = await fetch(url, {
579
+ headers: {
580
+ Authorization: `Bearer ${token}`,
581
+ Accept: "application/json"
582
+ },
583
+ signal: AbortSignal.timeout(DEFAULT_FETCH_TIMEOUT_MS)
584
+ });
585
+ if (!response.ok) {
586
+ throw new Error(`Google Directory groups.list failed (${response.status}): ${await response.text()}`);
587
+ }
588
+ const json = await response.json();
589
+ for (const group of json.groups ?? []) {
590
+ const mappedRoles = this.options.mapGroupToRoles?.(group) ?? [group.email];
591
+ for (const role of mappedRoles) {
592
+ if (role) roles.add(role);
593
+ }
594
+ }
595
+ pageToken = json.nextPageToken;
596
+ } while (pageToken);
597
+ return Array.from(roles);
598
+ }
599
+ async getToken() {
600
+ if (this.options.getAccessToken) {
601
+ return this.options.getAccessToken();
602
+ }
603
+ if (this.accessToken && Date.now() < this.tokenExpiresAt - 6e4) {
604
+ return this.accessToken;
605
+ }
606
+ if (this.options.serviceAccount) {
607
+ if (!this.tokenRefreshPromise) {
608
+ this.tokenRefreshPromise = this.getServiceAccountToken().finally(() => {
609
+ this.tokenRefreshPromise = void 0;
610
+ });
611
+ }
612
+ return this.tokenRefreshPromise;
613
+ }
614
+ if (this.accessToken) {
615
+ return this.accessToken;
616
+ }
617
+ throw new Error("Google Workspace Directory authentication is not configured.");
618
+ }
619
+ async getServiceAccountToken() {
620
+ const account = this.options.serviceAccount;
621
+ const now = Math.floor(Date.now() / 1e3);
622
+ const header = { alg: "RS256", typ: "JWT", ...account.privateKeyId ? { kid: account.privateKeyId } : {} };
623
+ const claim = {
624
+ iss: account.clientEmail,
625
+ scope: (account.scopes ?? DEFAULT_DIRECTORY_SCOPES).join(" "),
626
+ aud: OAUTH_TOKEN_URL,
627
+ exp: now + 3600,
628
+ iat: now,
629
+ ...account.subject ? { sub: account.subject } : {}
630
+ };
631
+ const unsigned = `${this.base64Url(JSON.stringify(header))}.${this.base64Url(JSON.stringify(claim))}`;
632
+ const privateKey = this.normalizePrivateKey(account.privateKey);
633
+ let signature;
634
+ try {
635
+ signature = createSign("RSA-SHA256").update(unsigned).sign(privateKey, "base64url");
636
+ } catch (err) {
637
+ const hasBegin = privateKey.includes("-----BEGIN");
638
+ const hasEnd = privateKey.includes("-----END");
639
+ throw new Error(
640
+ `Google service account private key signing failed (${err.message}). Key has BEGIN marker: ${hasBegin}, END marker: ${hasEnd}. Ensure your .env value contains the raw PEM with \\n for newlines, without extra surrounding quotes or commas.`
641
+ );
642
+ }
643
+ const response = await fetch(OAUTH_TOKEN_URL, {
644
+ method: "POST",
645
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
646
+ body: new URLSearchParams({
647
+ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
648
+ assertion: `${unsigned}.${signature}`
649
+ }),
650
+ signal: AbortSignal.timeout(DEFAULT_FETCH_TIMEOUT_MS)
651
+ });
652
+ if (!response.ok) {
653
+ throw new Error(`Google service account token request failed (${response.status}): ${await response.text()}`);
654
+ }
655
+ const json = await response.json();
656
+ this.accessToken = json.access_token;
657
+ this.tokenExpiresAt = Date.now() + json.expires_in * 1e3;
658
+ return json.access_token;
659
+ }
660
+ base64Url(value) {
661
+ return Buffer.from(value).toString("base64url");
662
+ }
663
+ normalizePrivateKey(key) {
664
+ let out = key.trim();
665
+ for (let i = 0; i < 5; i++) {
666
+ const before = out;
667
+ if (out.endsWith(",")) out = out.slice(0, -1).trim();
668
+ if (out.startsWith('"') && out.endsWith('"') || out.startsWith("'") && out.endsWith("'")) {
669
+ out = out.slice(1, -1);
670
+ }
671
+ if (out.startsWith('\\"') && out.endsWith('\\"') || out.startsWith("\\'") && out.endsWith("\\'")) {
672
+ out = out.slice(2, -2);
673
+ }
674
+ if (out === before) break;
675
+ }
676
+ out = out.replace(/\\n/g, "\n");
677
+ out = out.replace(/\\"/g, '"').replace(/\\'/g, "'");
678
+ out = out.replace(/\r\n?/g, "\n");
679
+ if (!out.endsWith("\n")) out += "\n";
680
+ return out;
681
+ }
682
+ };
683
+
684
+ export { MastraAuthGoogle, MastraRBACGoogle, mapGoogleClaimsToUser };
685
+ //# sourceMappingURL=index.js.map
686
+ //# sourceMappingURL=index.js.map