@neondatabase/auth 0.1.0-beta.13 → 0.1.0-beta.15

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.
@@ -1,5 +1,5 @@
1
1
  import { getGlobalBroadcastChannel } from "better-auth/client";
2
- import { adminClient, anonymousClient, emailOTPClient, jwtClient, organizationClient } from "better-auth/client/plugins";
2
+ import { adminClient, emailOTPClient, jwtClient, organizationClient } from "better-auth/client/plugins";
3
3
  import z from "zod";
4
4
 
5
5
  //#region src/core/in-flight-request-manager.ts
@@ -118,6 +118,14 @@ const CLOCK_SKEW_BUFFER_MS = 1e4;
118
118
  const DEFAULT_SESSION_EXPIRY_MS = 36e5;
119
119
  /** Name of the session verifier parameter in the URL, used for the OAUTH flow */
120
120
  const NEON_AUTH_SESSION_VERIFIER_PARAM_NAME = "neon_auth_session_verifier";
121
+ /** Name of the popup marker parameter in the URL, used for OAuth popup flow in iframes */
122
+ const NEON_AUTH_POPUP_PARAM_NAME = "neon_popup";
123
+ /** Name of the original callback URL parameter, used in OAuth popup flow */
124
+ const NEON_AUTH_POPUP_CALLBACK_PARAM_NAME = "neon_popup_callback";
125
+ /** The callback route used for OAuth popup completion (must be in middleware SKIP_ROUTES) */
126
+ const NEON_AUTH_POPUP_CALLBACK_ROUTE = "/auth/callback";
127
+ /** Message type for OAuth popup completion postMessage */
128
+ const OAUTH_POPUP_MESSAGE_TYPE = "neon-auth:oauth-complete";
121
129
 
122
130
  //#endregion
123
131
  //#region src/utils/jwt.ts
@@ -293,6 +301,59 @@ var AnonymousTokenCacheManager = class {
293
301
  }
294
302
  };
295
303
 
304
+ //#endregion
305
+ //#region src/core/oauth-popup.ts
306
+ /**
307
+ * Opens an OAuth popup window and waits for completion.
308
+ *
309
+ * This is used when the app is running inside an iframe, where OAuth
310
+ * redirect flows don't work due to X-Frame-Options/CSP restrictions.
311
+ * The popup completes the OAuth flow and sends a postMessage back
312
+ * with the session verifier needed to fetch the session.
313
+ *
314
+ * @param url - The OAuth authorization URL to open in the popup
315
+ * @returns Promise that resolves with the session verifier when OAuth completes
316
+ * @throws Error if popup is blocked, closed by user, or times out
317
+ */
318
+ async function openOAuthPopup(url) {
319
+ const timeout = 12e4;
320
+ const pollInterval = 500;
321
+ return new Promise((resolve, reject) => {
322
+ const popup = globalThis.open(url, "neon_oauth_popup", "width=500,height=700,popup=yes");
323
+ if (!popup || popup.closed) {
324
+ reject(/* @__PURE__ */ new Error("Popup blocked. Please allow popups for this site."));
325
+ return;
326
+ }
327
+ const timeoutId = setTimeout(() => {
328
+ cleanup();
329
+ try {
330
+ popup.close();
331
+ } catch {}
332
+ reject(/* @__PURE__ */ new Error("OAuth popup timed out. Please try again."));
333
+ }, timeout);
334
+ const pollId = setInterval(() => {
335
+ try {
336
+ if (popup.closed) {
337
+ cleanup();
338
+ reject(/* @__PURE__ */ new Error("OAuth popup was closed. Please try again."));
339
+ }
340
+ } catch {}
341
+ }, pollInterval);
342
+ function cleanup() {
343
+ clearTimeout(timeoutId);
344
+ clearInterval(pollId);
345
+ globalThis.removeEventListener("message", handleMessage);
346
+ }
347
+ function handleMessage(event) {
348
+ if (event.origin !== globalThis.location.origin) return;
349
+ if (event.data?.type !== OAUTH_POPUP_MESSAGE_TYPE) return;
350
+ cleanup();
351
+ resolve({ verifier: event.data.verifier || null });
352
+ }
353
+ globalThis.addEventListener("message", handleMessage);
354
+ });
355
+ }
356
+
296
357
  //#endregion
297
358
  //#region src/utils/browser.ts
298
359
  /**
@@ -302,6 +363,19 @@ var AnonymousTokenCacheManager = class {
302
363
  const isBrowser = () => {
303
364
  return globalThis.window !== void 0 && typeof document !== "undefined";
304
365
  };
366
+ /**
367
+ * Checks if the code is running inside an iframe
368
+ * Used to detect embedded contexts where OAuth redirect won't work
369
+ * @returns true if in iframe, false otherwise
370
+ */
371
+ const isIframe = () => {
372
+ if (!isBrowser()) return false;
373
+ try {
374
+ return globalThis.self !== globalThis.top;
375
+ } catch {
376
+ return true;
377
+ }
378
+ };
305
379
 
306
380
  //#endregion
307
381
  //#region src/plugins/anonymous-token.ts
@@ -341,6 +415,39 @@ const BETTER_AUTH_ENDPOINTS = {
341
415
  anonymousSignIn: "/sign-in/anonymous",
342
416
  anonymousToken: "/token/anonymous"
343
417
  };
418
+ /**
419
+ * Handles social sign-in via popup when running inside an iframe.
420
+ * This is necessary because OAuth redirects don't work in iframes due to
421
+ * X-Frame-Options/CSP restrictions from OAuth providers.
422
+ *
423
+ * Flow:
424
+ * 1. Request OAuth URL from server (with disableRedirect)
425
+ * 2. Open popup window with the OAuth URL
426
+ * 3. Wait for popup to complete and send back the session verifier
427
+ * 4. Navigate to callbackURL with verifier - normal page load handles session
428
+ */
429
+ async function handleSocialSignInViaPopup(input, init) {
430
+ const body = JSON.parse(init?.body || "{}");
431
+ const originalCallbackURL = body.callbackURL || "/";
432
+ const popupCallbackUrl = new URL(NEON_AUTH_POPUP_CALLBACK_ROUTE, globalThis.location.origin);
433
+ popupCallbackUrl.searchParams.set(NEON_AUTH_POPUP_PARAM_NAME, "1");
434
+ popupCallbackUrl.searchParams.set(NEON_AUTH_POPUP_CALLBACK_PARAM_NAME, originalCallbackURL);
435
+ body.callbackURL = popupCallbackUrl.toString();
436
+ body.disableRedirect = true;
437
+ const response = await fetch(input, {
438
+ ...init,
439
+ body: JSON.stringify(body)
440
+ });
441
+ const data = await response.json();
442
+ const oauthUrl = data.url;
443
+ if (!oauthUrl) throw new Error("Failed to get OAuth URL");
444
+ const popupResult = await openOAuthPopup(oauthUrl);
445
+ if (!popupResult.verifier) throw new Error("OAuth completed but no session verifier received");
446
+ const navigationUrl = new URL(originalCallbackURL, globalThis.location.origin);
447
+ navigationUrl.searchParams.set(NEON_AUTH_SESSION_VERIFIER_PARAM_NAME, popupResult.verifier);
448
+ globalThis.location.href = navigationUrl.toString();
449
+ return Response.json(data, { status: response.status });
450
+ }
344
451
  const BETTER_AUTH_METHODS_HOOKS = {
345
452
  signUp: {
346
453
  onRequest: () => {},
@@ -359,6 +466,10 @@ const BETTER_AUTH_METHODS_HOOKS = {
359
466
  }
360
467
  },
361
468
  signIn: {
469
+ beforeRequest: (input, init) => {
470
+ if (!(typeof input === "string" ? input : input.toString()).includes("/sign-in/social") || !isIframe()) return null;
471
+ return handleSocialSignInViaPopup(input, init);
472
+ },
362
473
  onRequest: () => {},
363
474
  onSuccess: (responseData) => {
364
475
  if (isSessionResponseData(responseData)) {
@@ -513,6 +624,20 @@ function deriveBetterAuthMethodFromUrl(url) {
513
624
  if (url.includes(BETTER_AUTH_ENDPOINTS.getSession) || url.includes(BETTER_AUTH_ENDPOINTS.token)) return "getSession";
514
625
  }
515
626
  function initBroadcastChannel() {
627
+ if (isBrowser() && globalThis.opener && globalThis.opener !== globalThis) {
628
+ const params = new URLSearchParams(globalThis.location.search);
629
+ if (params.has(NEON_AUTH_POPUP_PARAM_NAME)) {
630
+ const verifier = params.get(NEON_AUTH_SESSION_VERIFIER_PARAM_NAME);
631
+ const originalCallback = params.get(NEON_AUTH_POPUP_CALLBACK_PARAM_NAME);
632
+ globalThis.opener.postMessage({
633
+ type: OAUTH_POPUP_MESSAGE_TYPE,
634
+ verifier,
635
+ originalCallback
636
+ }, "*");
637
+ globalThis.close();
638
+ return;
639
+ }
640
+ }
516
641
  getGlobalBroadcastChannel().subscribe((message) => {
517
642
  if (message.clientId === CURRENT_TAB_CLIENT_ID) return;
518
643
  const trigger = message.data?.trigger;
@@ -528,7 +653,6 @@ const supportedBetterAuthClientPlugins = [
528
653
  adminClient(),
529
654
  organizationClient(),
530
655
  emailOTPClient(),
531
- anonymousClient(),
532
656
  anonymousTokenClient()
533
657
  ];
534
658
  var NeonAuthAdapterCore = class {
@@ -616,4 +740,4 @@ var NeonAuthAdapterCore = class {
616
740
  };
617
741
 
618
742
  //#endregion
619
- export { DEFAULT_SESSION_EXPIRY_MS as a, CURRENT_TAB_CLIENT_ID as i, BETTER_AUTH_METHODS_CACHE as n, BETTER_AUTH_METHODS_HOOKS as r, NeonAuthAdapterCore as t };
743
+ export { DEFAULT_SESSION_EXPIRY_MS as a, CURRENT_TAB_CLIENT_ID as i, BETTER_AUTH_METHODS_CACHE as n, NEON_AUTH_SESSION_VERIFIER_PARAM_NAME as o, BETTER_AUTH_METHODS_HOOKS as r, NeonAuthAdapterCore as t };