@mastra/auth-clerk 1.0.2-alpha.0 → 1.1.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
@@ -1,167 +1,116 @@
1
1
  import { createClerkClient } from '@clerk/backend';
2
2
  import { verifyJwks } from '@mastra/auth';
3
+ import { MastraAuthProvider } from '@mastra/core/server';
3
4
 
4
5
  // src/index.ts
5
-
6
- // ../../packages/core/dist/chunk-X2WMFSPB.js
7
- var RegisteredLogger = {
8
- LLM: "LLM"};
9
- var LogLevel = {
10
- DEBUG: "debug",
11
- INFO: "info",
12
- WARN: "warn",
13
- ERROR: "error"};
14
- var MastraLogger = class {
15
- name;
16
- level;
17
- transports;
18
- constructor(options = {}) {
19
- this.name = options.name || "Mastra";
20
- this.level = options.level || LogLevel.ERROR;
21
- this.transports = new Map(Object.entries(options.transports || {}));
22
- }
23
- getTransports() {
24
- return this.transports;
25
- }
26
- trackException(_error) {
27
- }
28
- async listLogs(transportId, params) {
29
- if (!transportId || !this.transports.has(transportId)) {
30
- return { logs: [], total: 0, page: params?.page ?? 1, perPage: params?.perPage ?? 100, hasMore: false };
31
- }
32
- return this.transports.get(transportId).listLogs(params) ?? {
33
- logs: [],
34
- total: 0,
35
- page: params?.page ?? 1,
36
- perPage: params?.perPage ?? 100,
37
- hasMore: false
38
- };
39
- }
40
- async listLogsByRunId({
41
- transportId,
42
- runId,
43
- fromDate,
44
- toDate,
45
- logLevel,
46
- filters,
47
- page,
48
- perPage
49
- }) {
50
- if (!transportId || !this.transports.has(transportId) || !runId) {
51
- return { logs: [], total: 0, page: page ?? 1, perPage: perPage ?? 100, hasMore: false };
52
- }
53
- return this.transports.get(transportId).listLogsByRunId({ runId, fromDate, toDate, logLevel, filters, page, perPage }) ?? {
54
- logs: [],
55
- total: 0,
56
- page: page ?? 1,
57
- perPage: perPage ?? 100,
58
- hasMore: false
59
- };
60
- }
61
- };
62
- var ConsoleLogger = class extends MastraLogger {
63
- constructor(options = {}) {
64
- super(options);
65
- }
66
- debug(message, ...args) {
67
- if (this.level === LogLevel.DEBUG) {
68
- console.info(message, ...args);
69
- }
70
- }
71
- info(message, ...args) {
72
- if (this.level === LogLevel.INFO || this.level === LogLevel.DEBUG) {
73
- console.info(message, ...args);
74
- }
75
- }
76
- warn(message, ...args) {
77
- if (this.level === LogLevel.WARN || this.level === LogLevel.INFO || this.level === LogLevel.DEBUG) {
78
- console.info(message, ...args);
79
- }
6
+ var DEFAULT_COOKIE_NAME = "clerk_session";
7
+ var DEFAULT_COOKIE_MAX_AGE = 86400;
8
+ var DEFAULT_SCOPES = ["openid", "profile", "email"];
9
+ var SALT_LENGTH = 16;
10
+ var IV_LENGTH = 12;
11
+ async function deriveKey(password, salt, usage) {
12
+ const encoder = new TextEncoder();
13
+ const keyMaterial = await crypto.subtle.importKey("raw", encoder.encode(password), "PBKDF2", false, [
14
+ "deriveBits",
15
+ "deriveKey"
16
+ ]);
17
+ return crypto.subtle.deriveKey(
18
+ { name: "PBKDF2", salt, iterations: 1e5, hash: "SHA-256" },
19
+ keyMaterial,
20
+ { name: "AES-GCM", length: 256 },
21
+ false,
22
+ [usage]
23
+ );
24
+ }
25
+ async function encryptSession(data, password) {
26
+ const encoder = new TextEncoder();
27
+ const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
28
+ const key = await deriveKey(password, salt, "encrypt");
29
+ const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
30
+ const encrypted = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, encoder.encode(JSON.stringify(data)));
31
+ const combined = new Uint8Array(salt.length + iv.length + new Uint8Array(encrypted).length);
32
+ combined.set(salt);
33
+ combined.set(iv, salt.length);
34
+ combined.set(new Uint8Array(encrypted), salt.length + iv.length);
35
+ return btoa(String.fromCharCode(...combined));
36
+ }
37
+ async function decryptSession(encrypted, password) {
38
+ const combined = Uint8Array.from(atob(encrypted), (c) => c.charCodeAt(0));
39
+ const salt = combined.slice(0, SALT_LENGTH);
40
+ const iv = combined.slice(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);
41
+ const data = combined.slice(SALT_LENGTH + IV_LENGTH);
42
+ const key = await deriveKey(password, salt, "decrypt");
43
+ const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, data);
44
+ return JSON.parse(new TextDecoder().decode(decrypted));
45
+ }
46
+ var STATE_TOKEN_EXPIRY_MS = 10 * 60 * 1e3;
47
+ async function hmacSign(data, secret) {
48
+ const encoder = new TextEncoder();
49
+ const keyData = encoder.encode(secret);
50
+ const dataBytes = encoder.encode(data);
51
+ const cryptoKey = await crypto.subtle.importKey("raw", keyData, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
52
+ const signature = await crypto.subtle.sign("HMAC", cryptoKey, dataBytes);
53
+ const sigBytes = new Uint8Array(signature);
54
+ return btoa(String.fromCharCode(...sigBytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
55
+ }
56
+ function timingSafeEqual(a, b) {
57
+ if (a.length !== b.length) return false;
58
+ let result = 0;
59
+ for (let i = 0; i < a.length; i++) {
60
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
80
61
  }
81
- error(message, ...args) {
82
- if (this.level === LogLevel.ERROR || this.level === LogLevel.WARN || this.level === LogLevel.INFO || this.level === LogLevel.DEBUG) {
83
- console.error(message, ...args);
84
- }
62
+ return result === 0;
63
+ }
64
+ async function createStateToken(originalState, redirectUri, secret) {
65
+ const payload = {
66
+ s: originalState,
67
+ r: redirectUri,
68
+ e: Date.now() + STATE_TOKEN_EXPIRY_MS
69
+ };
70
+ const payloadB64 = btoa(JSON.stringify(payload));
71
+ const signature = await hmacSign(payloadB64, secret);
72
+ return `${payloadB64}.${signature}`;
73
+ }
74
+ async function verifyStateToken(stateToken, secret) {
75
+ const parts = stateToken.split(".");
76
+ if (parts.length !== 2) {
77
+ throw new Error("Invalid state token format");
85
78
  }
86
- async listLogs(_transportId, _params) {
87
- return { logs: [], total: 0, page: _params?.page ?? 1, perPage: _params?.perPage ?? 100, hasMore: false };
79
+ const [payloadB64, signature] = parts;
80
+ const expectedSig = await hmacSign(payloadB64, secret);
81
+ if (!timingSafeEqual(signature, expectedSig)) {
82
+ throw new Error("Invalid state token signature");
88
83
  }
89
- async listLogsByRunId(_args) {
90
- return { logs: [], total: 0, page: _args.page ?? 1, perPage: _args.perPage ?? 100, hasMore: false };
84
+ const payload = JSON.parse(atob(payloadB64));
85
+ if (payload.e < Date.now()) {
86
+ throw new Error("State token has expired");
91
87
  }
92
- };
93
-
94
- // ../../packages/core/dist/chunk-WCAFTXGK.js
95
- var MastraBase = class {
96
- component = RegisteredLogger.LLM;
97
- logger;
98
- name;
99
- #rawConfig;
100
- constructor({
101
- component,
102
- name,
103
- rawConfig
104
- }) {
105
- this.component = component || RegisteredLogger.LLM;
106
- this.name = name;
107
- this.#rawConfig = rawConfig;
108
- this.logger = new ConsoleLogger({ name: `${this.component} - ${this.name}` });
109
- }
110
- /**
111
- * Returns the raw storage configuration this primitive was created from,
112
- * or undefined if it was created from code.
113
- */
114
- toRawConfig() {
115
- return this.#rawConfig;
116
- }
117
- /**
118
- * Sets the raw storage configuration for this primitive.
119
- * @internal
120
- */
121
- __setRawConfig(rawConfig) {
122
- this.#rawConfig = rawConfig;
123
- }
124
- /**
125
- * Set the logger for the agent
126
- * @param logger
127
- */
128
- __setLogger(logger) {
129
- this.logger = logger;
130
- if (this.component !== RegisteredLogger.LLM) {
131
- this.logger.debug(`Logger updated [component=${this.component}] [name=${this.name}]`);
132
- }
133
- }
134
- };
135
-
136
- // ../../packages/core/dist/server/index.js
137
- var MastraAuthProvider = class extends MastraBase {
138
- protected;
139
- public;
140
- constructor(options) {
141
- super({ component: "AUTH", name: options?.name });
142
- if (options?.authorizeUser) {
143
- this.authorizeUser = options.authorizeUser.bind(this);
144
- }
145
- this.protected = options?.protected;
146
- this.public = options?.public;
147
- }
148
- registerOptions(opts) {
149
- if (opts?.authorizeUser) {
150
- this.authorizeUser = opts.authorizeUser.bind(this);
151
- }
152
- if (opts?.protected) {
153
- this.protected = opts.protected;
154
- }
155
- if (opts?.public) {
156
- this.public = opts.public;
157
- }
158
- }
159
- };
160
-
161
- // src/index.ts
88
+ return { originalState: payload.s, redirectUri: payload.r };
89
+ }
90
+ function escapeRegex(str) {
91
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
92
+ }
93
+ function deriveFapiUrl(publishableKey) {
94
+ const withoutPrefix = publishableKey.replace(/^pk_(test|live)_/, "");
95
+ const decoded = atob(withoutPrefix);
96
+ const domain = decoded.replace(/\$$/, "");
97
+ return `https://${domain}`;
98
+ }
162
99
  var MastraAuthClerk = class extends MastraAuthProvider {
163
100
  clerk;
164
101
  jwksUri;
102
+ publishableKey;
103
+ fapiUrl;
104
+ // SSO fields
105
+ oauthClientId;
106
+ oauthClientSecret;
107
+ _redirectUri;
108
+ scopes;
109
+ cookieName;
110
+ cookieMaxAge;
111
+ cookiePassword;
112
+ secureCookies;
113
+ ssoEnabled;
165
114
  constructor(options) {
166
115
  super({ name: options?.name ?? "clerk" });
167
116
  const jwksUri = options?.jwksUri ?? process.env.CLERK_JWKS_URI;
@@ -173,18 +122,317 @@ var MastraAuthClerk = class extends MastraAuthProvider {
173
122
  );
174
123
  }
175
124
  this.jwksUri = jwksUri;
125
+ this.publishableKey = publishableKey;
126
+ this.fapiUrl = deriveFapiUrl(publishableKey);
176
127
  this.clerk = createClerkClient({
177
128
  secretKey,
178
129
  publishableKey
179
130
  });
131
+ const oauthClientId = options?.oauthClientId ?? process.env.CLERK_OAUTH_CLIENT_ID;
132
+ const oauthClientSecret = options?.oauthClientSecret ?? process.env.CLERK_OAUTH_CLIENT_SECRET;
133
+ const redirectUri = options?.redirectUri ?? process.env.CLERK_OAUTH_REDIRECT_URI;
134
+ const cookiePassword = options?.session?.cookiePassword ?? process.env.CLERK_COOKIE_PASSWORD ?? crypto.randomUUID() + crypto.randomUUID();
135
+ this.oauthClientId = oauthClientId ?? null;
136
+ this.oauthClientSecret = oauthClientSecret ?? null;
137
+ this._redirectUri = redirectUri ?? null;
138
+ this.scopes = options?.scopes ?? DEFAULT_SCOPES;
139
+ this.cookieName = options?.session?.cookieName ?? DEFAULT_COOKIE_NAME;
140
+ this.cookieMaxAge = options?.session?.cookieMaxAge ?? DEFAULT_COOKIE_MAX_AGE;
141
+ this.cookiePassword = cookiePassword;
142
+ this.secureCookies = options?.session?.secureCookies ?? process.env.NODE_ENV === "production";
143
+ this.ssoEnabled = !!(oauthClientId && oauthClientSecret);
144
+ if (this.ssoEnabled) {
145
+ if (cookiePassword.length < 32) {
146
+ throw new Error(
147
+ "Cookie password must be at least 32 characters for SSO. Set CLERK_COOKIE_PASSWORD environment variable."
148
+ );
149
+ }
150
+ if (!options?.session?.cookiePassword && !process.env.CLERK_COOKIE_PASSWORD) {
151
+ console.warn(
152
+ "[MastraAuthClerk] No cookie password set \u2014 using auto-generated value. Sessions will not survive restarts. Set CLERK_COOKIE_PASSWORD for production use."
153
+ );
154
+ }
155
+ this._attachSSOProvider();
156
+ this._attachSessionProvider();
157
+ }
180
158
  this.registerOptions(options);
181
159
  }
182
- async authenticateToken(token) {
183
- const user = await verifyJwks(token, this.jwksUri);
184
- return user;
160
+ // ============================================================================
161
+ // MastraAuthProvider Implementation
162
+ // ============================================================================
163
+ async authenticateToken(token, request) {
164
+ if (this.ssoEnabled && request) {
165
+ const sessionUser = await this.getUserFromSessionCookie(request);
166
+ if (sessionUser) return sessionUser;
167
+ }
168
+ if (!token || typeof token !== "string") {
169
+ return null;
170
+ }
171
+ try {
172
+ const user = await verifyJwks(token, this.jwksUri);
173
+ return user;
174
+ } catch {
175
+ return null;
176
+ }
185
177
  }
186
178
  async authorizeUser(user) {
187
- return !!user.sub;
179
+ return !!(user.sub || user.id);
180
+ }
181
+ // ============================================================================
182
+ // IUserProvider Implementation
183
+ // ============================================================================
184
+ /**
185
+ * Extract the bearer token from the request's Authorization header or __session cookie.
186
+ */
187
+ extractToken(request) {
188
+ const authHeader = request.headers.get("Authorization");
189
+ if (authHeader) {
190
+ const token = authHeader.replace(/^Bearer\s+/i, "").trim();
191
+ if (token) return token;
192
+ }
193
+ const cookie = request.headers.get("Cookie");
194
+ if (cookie) {
195
+ const match = cookie.match(/__session=([^;]+)/);
196
+ if (match?.[1]) return match[1];
197
+ }
198
+ return null;
199
+ }
200
+ async getCurrentUser(request) {
201
+ if (this.ssoEnabled) {
202
+ const sessionUser = await this.getUserFromSessionCookie(request);
203
+ if (sessionUser) return sessionUser;
204
+ }
205
+ const token = this.extractToken(request);
206
+ if (!token) return null;
207
+ try {
208
+ const payload = await this.authenticateToken(token);
209
+ if (!payload?.sub) return null;
210
+ try {
211
+ const clerkUser = await this.clerk.users.getUser(payload.sub);
212
+ return {
213
+ id: clerkUser.id,
214
+ email: clerkUser.emailAddresses?.[0]?.emailAddress,
215
+ name: [clerkUser.firstName, clerkUser.lastName].filter(Boolean).join(" ") || void 0,
216
+ avatarUrl: clerkUser.imageUrl,
217
+ metadata: clerkUser.publicMetadata
218
+ };
219
+ } catch {
220
+ return {
221
+ id: payload.sub,
222
+ email: payload.email ?? void 0,
223
+ name: payload.name ?? void 0
224
+ };
225
+ }
226
+ } catch {
227
+ return null;
228
+ }
229
+ }
230
+ async getUser(userId) {
231
+ try {
232
+ const clerkUser = await this.clerk.users.getUser(userId);
233
+ return {
234
+ id: clerkUser.id,
235
+ email: clerkUser.emailAddresses?.[0]?.emailAddress,
236
+ name: [clerkUser.firstName, clerkUser.lastName].filter(Boolean).join(" ") || void 0,
237
+ avatarUrl: clerkUser.imageUrl,
238
+ metadata: clerkUser.publicMetadata
239
+ };
240
+ } catch {
241
+ return null;
242
+ }
243
+ }
244
+ getUserProfileUrl(user) {
245
+ return `/user/${user.id}`;
246
+ }
247
+ // ============================================================================
248
+ // Helper Methods
249
+ // ============================================================================
250
+ /**
251
+ * Check if SSO is enabled (OAuth credentials are configured).
252
+ */
253
+ isSSOEnabled() {
254
+ return this.ssoEnabled;
255
+ }
256
+ /**
257
+ * Get the derived Frontend API URL.
258
+ */
259
+ getFapiUrl() {
260
+ return this.fapiUrl;
261
+ }
262
+ /**
263
+ * Build consistent cookie attribute string for set/clear operations.
264
+ */
265
+ cookieFlags(maxAge) {
266
+ const flags = `Path=/; HttpOnly; SameSite=Lax; Max-Age=${maxAge}`;
267
+ return this.secureCookies ? `${flags}; Secure` : flags;
268
+ }
269
+ /**
270
+ * Extract user from the encrypted SSO session cookie.
271
+ */
272
+ async getUserFromSessionCookie(request) {
273
+ const cookie = "header" in request && typeof request.header === "function" ? request.header("cookie") : request.headers?.get("cookie");
274
+ if (!cookie) return null;
275
+ const match = cookie.match(new RegExp(`(?:^|;\\s*)${escapeRegex(this.cookieName)}=([^;]+)`));
276
+ if (!match?.[1]) return null;
277
+ try {
278
+ const sessionData = await decryptSession(decodeURIComponent(match[1]), this.cookiePassword);
279
+ if (sessionData.expiresAt < Date.now()) {
280
+ return null;
281
+ }
282
+ return sessionData.user;
283
+ } catch {
284
+ return null;
285
+ }
286
+ }
287
+ // ============================================================================
288
+ // Dynamic ISSOProvider attachment (only when OAuth is configured)
289
+ // ============================================================================
290
+ /**
291
+ * Dynamically attach ISSOProvider methods to this instance.
292
+ * This ensures duck-typing detection only finds these methods when SSO is configured.
293
+ */
294
+ _attachSSOProvider() {
295
+ const self = this;
296
+ this.getLoginUrl = async function(redirectUri, state) {
297
+ const actualRedirectUri = redirectUri ?? self._redirectUri;
298
+ if (!actualRedirectUri) {
299
+ throw new Error("Redirect URI is required for SSO login");
300
+ }
301
+ const signedState = await createStateToken(state, actualRedirectUri, self.cookiePassword);
302
+ const params = new URLSearchParams({
303
+ client_id: self.oauthClientId,
304
+ response_type: "code",
305
+ scope: self.scopes.join(" "),
306
+ redirect_uri: actualRedirectUri,
307
+ state: signedState
308
+ });
309
+ return `${self.fapiUrl}/oauth/authorize?${params.toString()}`;
310
+ };
311
+ this.handleCallback = async function(code, stateToken) {
312
+ const { redirectUri } = await verifyStateToken(stateToken, self.cookiePassword);
313
+ const tokenResponse = await fetch(`${self.fapiUrl}/oauth/token`, {
314
+ method: "POST",
315
+ headers: {
316
+ "Content-Type": "application/x-www-form-urlencoded",
317
+ Authorization: `Basic ${btoa(`${self.oauthClientId}:${self.oauthClientSecret}`)}`
318
+ },
319
+ body: new URLSearchParams({
320
+ grant_type: "authorization_code",
321
+ code,
322
+ redirect_uri: redirectUri
323
+ }),
324
+ signal: AbortSignal.timeout(1e4)
325
+ // 10 second timeout
326
+ });
327
+ if (!tokenResponse.ok) {
328
+ const error = await tokenResponse.text();
329
+ throw new Error(`Token exchange failed: ${error}`);
330
+ }
331
+ const tokens = await tokenResponse.json();
332
+ let user;
333
+ if (tokens.id_token) {
334
+ const payload = await verifyJwks(tokens.id_token, self.jwksUri);
335
+ user = {
336
+ id: payload.sub,
337
+ email: payload.email ?? void 0,
338
+ name: payload.name ?? void 0,
339
+ avatarUrl: payload.picture ?? void 0
340
+ };
341
+ } else {
342
+ const userInfoResponse = await fetch(`${self.fapiUrl}/oauth/userinfo`, {
343
+ headers: { Authorization: `Bearer ${tokens.access_token}` },
344
+ signal: AbortSignal.timeout(1e4)
345
+ // 10 second timeout
346
+ });
347
+ if (!userInfoResponse.ok) {
348
+ throw new Error("Failed to fetch user info from Clerk");
349
+ }
350
+ const userInfo = await userInfoResponse.json();
351
+ user = {
352
+ id: userInfo.sub,
353
+ email: userInfo.email,
354
+ name: userInfo.name,
355
+ avatarUrl: userInfo.picture
356
+ };
357
+ }
358
+ try {
359
+ const fullUser = await self.getUser(user.id);
360
+ if (fullUser) {
361
+ user = fullUser;
362
+ }
363
+ } catch {
364
+ }
365
+ const sessionData = {
366
+ user,
367
+ expiresAt: Date.now() + self.cookieMaxAge * 1e3
368
+ };
369
+ const encryptedSession = await encryptSession(sessionData, self.cookiePassword);
370
+ const cookieValue = `${self.cookieName}=${encodeURIComponent(encryptedSession)}; ${self.cookieFlags(self.cookieMaxAge)}`;
371
+ return {
372
+ user,
373
+ tokens: {
374
+ accessToken: tokens.access_token,
375
+ refreshToken: tokens.refresh_token,
376
+ idToken: tokens.id_token,
377
+ expiresAt: new Date(Date.now() + tokens.expires_in * 1e3)
378
+ },
379
+ cookies: [cookieValue]
380
+ };
381
+ };
382
+ this.getLoginButtonConfig = function() {
383
+ return {
384
+ provider: "clerk",
385
+ text: "Sign in with Clerk",
386
+ description: "Sign in using your Clerk account"
387
+ };
388
+ };
389
+ this.getLoginCookies = function(_state) {
390
+ return [];
391
+ };
392
+ this.getLogoutUrl = async function(_redirectUri, _request) {
393
+ return null;
394
+ };
395
+ }
396
+ // ============================================================================
397
+ // Dynamic ISessionProvider attachment (only when OAuth is configured)
398
+ // ============================================================================
399
+ /**
400
+ * Dynamically attach ISessionProvider methods to this instance.
401
+ */
402
+ _attachSessionProvider() {
403
+ const self = this;
404
+ this.createSession = async function(userId, metadata) {
405
+ const now = /* @__PURE__ */ new Date();
406
+ return {
407
+ id: crypto.randomUUID(),
408
+ userId,
409
+ createdAt: now,
410
+ expiresAt: new Date(now.getTime() + self.cookieMaxAge * 1e3),
411
+ metadata
412
+ };
413
+ };
414
+ this.validateSession = async function(_sessionId) {
415
+ return null;
416
+ };
417
+ this.destroySession = async function(_sessionId) {
418
+ };
419
+ this.refreshSession = async function(_sessionId) {
420
+ return null;
421
+ };
422
+ this.getSessionIdFromRequest = function(request) {
423
+ const cookie = request.headers.get("Cookie");
424
+ if (!cookie) return null;
425
+ const match = cookie.match(new RegExp(`(?:^|;\\s*)${escapeRegex(self.cookieName)}=([^;]+)`));
426
+ return match?.[1] ? decodeURIComponent(match[1]) : null;
427
+ };
428
+ this.getSessionHeaders = function(_session) {
429
+ return {};
430
+ };
431
+ this.getClearSessionHeaders = function() {
432
+ return {
433
+ "Set-Cookie": `${self.cookieName}=; ${self.cookieFlags(0)}`
434
+ };
435
+ };
188
436
  }
189
437
  };
190
438