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