@nominalso/vibe-auth 0.2.1 → 0.2.3

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.cjs CHANGED
@@ -89,7 +89,7 @@ function parseCallbackMessage(data) {
89
89
  }
90
90
  return null;
91
91
  }
92
- function createSilentAuth(config, getHostPrincipal) {
92
+ function createSilentAuth(config, getHostPrincipal, onBound) {
93
93
  const { supabase, provider, callbackPath, timeouts, lockName, boundUserKey } = config;
94
94
  async function getSessionUserId() {
95
95
  const { data } = await supabase.auth.getSession();
@@ -127,7 +127,10 @@ function createSilentAuth(config, getHostPrincipal) {
127
127
  if (priorUserId !== null && sessionUserId === priorUserId) return false;
128
128
  }
129
129
  const host = getHostPrincipal?.();
130
- if (host) writeBoundMarker(host);
130
+ if (host) {
131
+ writeBoundMarker(host);
132
+ onBound?.(host);
133
+ }
131
134
  return true;
132
135
  }
133
136
  function runHiddenAuthFrame(url) {
@@ -275,6 +278,7 @@ function createSilentAuth(config, getHostPrincipal) {
275
278
  ensureSession,
276
279
  rebindSession,
277
280
  isBoundTo,
281
+ stampBoundMarker: writeBoundMarker,
278
282
  clearBoundUser,
279
283
  completeWithResult
280
284
  };
@@ -549,6 +553,14 @@ function createAuthGate(config, silentAuth, interactiveAuth, hostAuth) {
549
553
  let applyGen = 0;
550
554
  let pendingSessionRead;
551
555
  let acceptPrincipalUpdates = !hostAuth?.isHostWired();
556
+ let redeemedHere = false;
557
+ const sessionBelongsTo = (principal, sessionOk) => {
558
+ if (!sessionOk) return false;
559
+ if (silentAuth.isBoundTo(principal)) return true;
560
+ if (!redeemedHere) return false;
561
+ silentAuth.stampBoundMarker(principal);
562
+ return true;
563
+ };
552
564
  const applyPrincipal = async (principal) => {
553
565
  hostPrincipal = principal;
554
566
  const gen = ++applyGen;
@@ -561,10 +573,11 @@ function createAuthGate(config, silentAuth, interactiveAuth, hostAuth) {
561
573
  sessionOk = await silentAuth.hasValidSession().catch(() => false);
562
574
  }
563
575
  if (cancelled || gen !== applyGen) return;
564
- if (sessionOk && silentAuth.isBoundTo(principal)) {
576
+ if (sessionBelongsTo(principal, sessionOk)) {
565
577
  setStatus("authenticated" /* Authenticated */);
566
578
  return;
567
579
  }
580
+ if (RETURNING_FROM_OAUTH) return;
568
581
  const ok = await silentAuth.rebindSession(principal).catch(() => false);
569
582
  if (cancelled || gen !== applyGen) return;
570
583
  if (!ok) void supabase.auth.signOut().catch(() => {
@@ -579,12 +592,13 @@ function createAuthGate(config, silentAuth, interactiveAuth, hostAuth) {
579
592
  if (principal) {
580
593
  const sessionOk = await silentAuth.hasValidSession().catch(() => false);
581
594
  if (cancelled || gen !== applyGen) return;
582
- if (!(sessionOk && silentAuth.isBoundTo(principal))) return;
595
+ if (!sessionBelongsTo(principal, sessionOk)) return;
583
596
  }
584
597
  setStatus("authenticated" /* Authenticated */);
585
598
  };
586
599
  async function resolve() {
587
600
  if (RETURNING_FROM_OAUTH) {
601
+ acceptPrincipalUpdates = true;
588
602
  oauthTimer = window.setTimeout(() => {
589
603
  if (!cancelled)
590
604
  setStatus((s) => s === "checking" /* Checking */ ? "unauthenticated" /* Unauthenticated */ : s);
@@ -629,8 +643,10 @@ function createAuthGate(config, silentAuth, interactiveAuth, hostAuth) {
629
643
  window.addEventListener("storage", onStorage);
630
644
  const { data: sub } = supabase.auth.onAuthStateChange((event, session) => {
631
645
  if (cancelled || event === "INITIAL_SESSION") return;
632
- if (session) void openIfBound();
633
- else {
646
+ if (session) {
647
+ if (RETURNING_FROM_OAUTH && event === "SIGNED_IN") redeemedHere = true;
648
+ void openIfBound();
649
+ } else {
634
650
  pendingSessionRead = void 0;
635
651
  setStatus((s) => s === "authenticated" /* Authenticated */ ? "unauthenticated" /* Unauthenticated */ : s);
636
652
  }
@@ -716,12 +732,58 @@ function createHostAuth(config, silentAuth) {
716
732
  };
717
733
  }
718
734
 
735
+ // src/wireVibeApp.ts
736
+ function createWireVibeApp(config, hostAuth) {
737
+ return function wireVibeApp(bridge, options = {}) {
738
+ const listeners = /* @__PURE__ */ new Set();
739
+ let current = null;
740
+ function publish(next) {
741
+ current = next;
742
+ for (const listener of [...listeners]) listener(next);
743
+ }
744
+ bridge.onContextChange((next) => {
745
+ publish(next);
746
+ options.onContextChange?.(next);
747
+ });
748
+ if (options.onSubrouteRequest) bridge.onSubrouteRequest?.(options.onSubrouteRequest);
749
+ hostAuth.wireHostAuth(bridge);
750
+ const noHost = typeof window === "undefined" || window.location.pathname === config.callbackPath;
751
+ const hostContext = noHost ? Promise.resolve(null) : bridge.connect().then((ctx) => {
752
+ const next = ctx;
753
+ hostAuth.seedLastUserId(next.user.id, next.tenant);
754
+ if (next.enableDataReset && options.onDataReset) {
755
+ bridge.onDataReset?.(options.onDataReset);
756
+ }
757
+ publish(next);
758
+ return next;
759
+ }).catch((error) => {
760
+ console.warn("[vibe-auth] host connect failed", error);
761
+ return null;
762
+ });
763
+ return {
764
+ bridge,
765
+ hostContext,
766
+ getHostContext: () => current,
767
+ subscribeHostContext: (listener) => {
768
+ listeners.add(listener);
769
+ return () => {
770
+ listeners.delete(listener);
771
+ };
772
+ }
773
+ };
774
+ };
775
+ }
776
+
719
777
  // src/createVibeAuth.ts
720
778
  function createVibeAuth(userConfig) {
721
779
  const config = resolveConfig(userConfig);
722
780
  assertPkce(config.supabase);
723
781
  let hostAuth;
724
- const silentAuth = createSilentAuth(config, () => hostAuth.getPrincipal());
782
+ const silentAuth = createSilentAuth(
783
+ config,
784
+ () => hostAuth.getPrincipal(),
785
+ (principal) => hostAuth.seedLastUserId(principal.userId, principal.tenant)
786
+ );
725
787
  const interactiveAuth = createInteractiveAuth(config, silentAuth);
726
788
  const { SilentCallback } = createCallbackHandler();
727
789
  hostAuth = createHostAuth(config, silentAuth);
@@ -742,6 +804,7 @@ function createVibeAuth(userConfig) {
742
804
  AuthGate,
743
805
  DefaultSignInScreen,
744
806
  SilentCallback,
807
+ wireVibeApp: createWireVibeApp(config, hostAuth),
745
808
  wireHostAuth,
746
809
  seedLastUserId,
747
810
  RESULT_MESSAGE_TYPE,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/config.ts","../src/authMessage.ts","../src/silentAuth.ts","../src/previewToken.ts","../src/signInResult.ts","../src/interactive.ts","../src/SilentCallback.tsx","../src/AuthGate.tsx","../src/hostAuth.ts","../src/createVibeAuth.ts"],"sourcesContent":["export { createVibeAuth, type VibeAuth } from './createVibeAuth'\nexport type {\n VibeAuthConfig,\n VibeAuthTimeouts,\n ResolvedVibeAuthConfig,\n ResolvedVibeAuthTimeouts,\n} from './config'\nexport type { AuthGateProps } from './AuthGate'\nexport type { AuthBridgeLike } from './hostAuth'\nexport type { HostPrincipal } from './principal'\nexport { AuthResultKind, type AuthResult } from './silentAuth'\nexport {\n SignInKind,\n SignInFailureReason,\n type SignInResult,\n type SignInAuthenticated,\n type SignInRedirecting,\n type SignInFailed,\n} from './signInResult'\nexport { RESULT_MESSAGE_TYPE } from './authMessage'\n","import type { SupabaseClient } from '@supabase/supabase-js'\n\n/**\n * Timeout knobs for the silent-SSO flow. All optional — sane defaults (below)\n * are the same values the silent-supabase-oidc-login skill shipped after its\n * production incidents, so only override these if you've measured a reason to.\n */\nexport interface VibeAuthTimeouts {\n /**\n * Bounds the hidden-iframe wait for a `prompt=none` result. Does NOT bound\n * the whole silent attempt — see `silentFlowCapMs`.\n * @default 10_000\n */\n silentIframeMs?: number\n /**\n * Hard cap on ONE WHOLE silent attempt (the `signInWithOAuth` fetch, the\n * hidden-iframe wait, and the code exchange). Without this, a slow attempt\n * could hold the cross-document lock past every waiter's patience.\n * @default 15_000\n */\n silentFlowCapMs?: number\n /**\n * How long a document queues for the cross-document Web Lock before giving\n * up. Sized off `silentFlowCapMs` by default (two full holder turns plus\n * margin) — pass this explicitly only if you also override `silentFlowCapMs`\n * and want the derived relationship preserved.\n * @default 2 * silentFlowCapMs + 5_000\n */\n lockWaitMs?: number\n /**\n * Bounds the wait for `detectSessionInUrl` to finish a top-level OAuth\n * return before the gate falls through to the sign-in screen.\n * @default 15_000\n */\n oauthCompletionMs?: number\n /** Bounds the interactive popup flow (user-facing — generous on purpose). @default 120_000 */\n popupMs?: number\n /** Bounds the Lovable-preview \"prime the popup\" navigation. @default 8_000 */\n previewPrimingMs?: number\n /** Poll interval while waiting for the preview-priming navigation to settle. @default 250 */\n previewPrimingPollMs?: number\n}\n\nexport interface ResolvedVibeAuthTimeouts {\n silentIframeMs: number\n silentFlowCapMs: number\n lockWaitMs: number\n oauthCompletionMs: number\n popupMs: number\n previewPrimingMs: number\n previewPrimingPollMs: number\n}\n\nexport const DEFAULT_TIMEOUTS = {\n silentIframeMs: 10_000,\n silentFlowCapMs: 15_000,\n oauthCompletionMs: 15_000,\n popupMs: 120_000,\n previewPrimingMs: 8_000,\n previewPrimingPollMs: 250,\n} as const\n\n/** Config passed to `createVibeAuth`. */\nexport interface VibeAuthConfig {\n /**\n * The app's own Supabase client — injected, never created by this package.\n * MUST have `auth.flowType: 'pkce'` (checked at init; see `assertPkce`). Own\n * this client for the same reason a generated app owns its `client.ts`: it\n * carries app-specific types, storage adapters, and Lovable Cloud wiring.\n */\n supabase: SupabaseClient\n /**\n * The IdP-federation provider name for `signInWithOAuth`, e.g.\n * `'custom:supabase-fedapp'`. Never guess this — it comes from the app's own\n * Supabase Auth → Providers configuration.\n */\n provider: string\n /**\n * Path of the same-origin OAuth callback route. Must match the app's actual\n * route AND the `redirect_to` the app's Supabase project allowlists.\n * @default '/silent-callback'\n */\n callbackPath?: string\n timeouts?: VibeAuthTimeouts\n /**\n * Prefix for the cross-document Web Lock name and the identity-switch\n * bound-user localStorage key. Only change this if multiple vibe apps with\n * DIFFERENT Supabase projects share one origin (rare) and you need their\n * locks/markers to not collide — same-project apps on different origins\n * never collide regardless, since Web Locks and localStorage are\n * origin-scoped.\n * @default 'nominal'\n */\n storageKeyPrefix?: string\n}\n\nexport interface ResolvedVibeAuthConfig {\n supabase: SupabaseClient\n provider: string\n callbackPath: string\n timeouts: ResolvedVibeAuthTimeouts\n lockName: string\n boundUserKey: string\n}\n\nexport function resolveConfig(config: VibeAuthConfig): ResolvedVibeAuthConfig {\n const callbackPath = config.callbackPath ?? '/silent-callback'\n const prefix = config.storageKeyPrefix ?? 'nominal'\n\n const silentFlowCapMs = config.timeouts?.silentFlowCapMs ?? DEFAULT_TIMEOUTS.silentFlowCapMs\n const lockWaitMs = config.timeouts?.lockWaitMs ?? 2 * silentFlowCapMs + 5_000\n\n return {\n supabase: config.supabase,\n provider: config.provider,\n callbackPath,\n timeouts: {\n silentIframeMs: config.timeouts?.silentIframeMs ?? DEFAULT_TIMEOUTS.silentIframeMs,\n silentFlowCapMs,\n lockWaitMs,\n oauthCompletionMs: config.timeouts?.oauthCompletionMs ?? DEFAULT_TIMEOUTS.oauthCompletionMs,\n popupMs: config.timeouts?.popupMs ?? DEFAULT_TIMEOUTS.popupMs,\n previewPrimingMs: config.timeouts?.previewPrimingMs ?? DEFAULT_TIMEOUTS.previewPrimingMs,\n previewPrimingPollMs:\n config.timeouts?.previewPrimingPollMs ?? DEFAULT_TIMEOUTS.previewPrimingPollMs,\n },\n lockName: `${prefix}-silent-sso`,\n boundUserKey: `${prefix}-sso-bound-user`,\n }\n}\n\n/**\n * Best-effort PKCE check. `flowType` isn't in supabase-js's public types (it's\n * an internal client option), so this reads it defensively and never throws —\n * a failed check must not break app boot, only warn loudly. This is the\n * runtime version of the skill's biggest \"verify this manually\" marker.\n */\nexport function assertPkce(supabase: SupabaseClient): void {\n try {\n const flowType = (supabase.auth as unknown as { flowType?: string }).flowType\n if (flowType !== 'pkce') {\n console.error(\n `[vibe-auth] Supabase client is not configured with { auth: { flowType: 'pkce' } } ` +\n `(got: ${String(flowType)}). This is a security control, not a preference — with ` +\n 'implicit flow the OAuth redirect carries a live access_token/refresh_token in the ' +\n \"URL, so an attacker-controlled redirect target yields a full session. Fix the app's \" +\n 'createClient(...) call before shipping.',\n )\n }\n } catch {\n // Never let a defensive check break app boot.\n }\n}\n","// No imports, on purpose. The callback route must not transitively import the\n// Supabase client — importing it boots the client in the callback window, and\n// with `detectSessionInUrl` (on by default) it will consume and strip the URL.\n//\n// This exact string is wire-compatible with every app generated from the\n// silent-supabase-oidc-login skill before this package existed — do not change\n// it; a migrated app must keep working alongside a not-yet-migrated sibling\n// document (hidden iframe, popup) on the same origin.\nexport const RESULT_MESSAGE_TYPE = 'silent-auth-result'\n","// Core of the silent-SSO flow — ported from the pre-package\n// silent-supabase-oidc-login skill code (see that SKILL.md's git history).\n// Module-scoped state in the skill (per-document; the\n// skill's apps are one document = one module graph) becomes factory-closure\n// state here for the same reason: `createVibeAuth` is called once per app,\n// so the closure IS the per-document scope.\nimport type { ResolvedVibeAuthConfig } from './config'\nimport type { HostPrincipal } from './principal'\nimport { RESULT_MESSAGE_TYPE } from './authMessage'\n\nconst SIBLING_SESSION_SYNC_MS = 50\n\nexport enum AuthResultKind {\n Code = 'code',\n}\n\n// ONE variant, on purpose. PKCE is mandatory (enforced by `assertPkce` at\n// init), so the only thing a callback can ever carry is an authorization\n// code. Do NOT add a `tokens` variant: that is implicit flow, and a token\n// payload can only mean the client config regressed — see\n// `parseCallbackMessage` below, which warns instead.\nexport type AuthResult = { kind: AuthResultKind.Code; code: string }\n\ntype CallbackMessage = {\n type?: string\n code?: string | null\n accessToken?: string | null\n refreshToken?: string | null\n error?: string | null\n} | null\n\n/** Normalize a postMessage payload from the callback route into an AuthResult. */\nexport function parseCallbackMessage(data: CallbackMessage): AuthResult | null {\n if (!data || data.type !== RESULT_MESSAGE_TYPE) return null\n if (data.code) return { kind: AuthResultKind.Code, code: data.code }\n if (data.accessToken || data.refreshToken) {\n // PKCE is enforced on the client, so the provider MUST return a code. Do\n // not \"helpfully\" accept tokens here — that silently re-enables implicit\n // flow. Warn loudly instead: it means the client config regressed.\n console.warn('[vibe-auth] ignoring implicit-flow token payload; PKCE expects ?code=')\n }\n return null\n}\n\nexport interface SilentAuth {\n hasValidSession(): Promise<boolean>\n trySilentSignIn(): Promise<boolean>\n ensureSession(): Promise<boolean>\n rebindSession(principal: HostPrincipal): Promise<boolean>\n /**\n * True when the bound-user marker is this host principal. No TTL — used by\n * `AuthGate` on load so a same-principal refresh can open without running\n * silent SSO. Live switch waiters still use a young-marker check inside\n * `rebindSession`.\n */\n isBoundTo(principal: HostPrincipal): boolean\n clearBoundUser(): void\n /** @internal exposed for interactive.ts, which shares completeWithResult/parseCallbackMessage's private helpers */\n completeWithResult(result: AuthResult): Promise<boolean>\n}\n\nexport function createSilentAuth(\n config: ResolvedVibeAuthConfig,\n // The host principal is the only identity we may stamp after THIS document\n // redeems a code — storage/SIGNED_IN must not stamp leftovers.\n getHostPrincipal?: () => HostPrincipal | undefined,\n): SilentAuth {\n const { supabase, provider, callbackPath, timeouts, lockName, boundUserKey } = config\n\n /**\n * The current session's Supabase user id, or null when none exists.\n * `getSession()` already handles expiry (refreshing an expired session and\n * returning null when that fails), so \"non-null\" here means a live session.\n */\n async function getSessionUserId(): Promise<string | null> {\n const { data } = await supabase.auth.getSession()\n return data.session?.user?.id ?? null\n }\n\n /** True when a Supabase session already exists locally. */\n async function hasValidSession(): Promise<boolean> {\n return (await getSessionUserId()) !== null\n }\n\n // A Web Lock is released immediately after the winner stores its session,\n // but Supabase's BroadcastChannel update can reach a waiting client a task\n // later. One short grace check avoids minting a redundant PKCE flow.\n async function waitForSiblingSession(): Promise<boolean> {\n if (await hasValidSession()) return true\n await new Promise((resolve) => setTimeout(resolve, SIBLING_SESSION_SYNC_MS))\n return hasValidSession()\n }\n\n type BoundMarker = { userId?: string; tenant?: string; at?: number }\n\n function readBoundMarker(): BoundMarker | null {\n if (typeof window === 'undefined') return null\n try {\n return JSON.parse(localStorage.getItem(boundUserKey) ?? 'null') as BoundMarker | null\n } catch {\n return null\n }\n }\n\n function isBoundTo(principal: HostPrincipal): boolean {\n const m = readBoundMarker()\n return m?.userId === principal.userId && m?.tenant === principal.tenant\n }\n\n function writeBoundMarker({ userId, tenant }: HostPrincipal): void {\n if (typeof window === 'undefined') return\n localStorage.setItem(boundUserKey, JSON.stringify({ userId, tenant, at: Date.now() }))\n }\n\n /**\n * Redeem an authorization code relayed from the callback route.\n *\n * KEEP THIS even though `detectSessionInUrl` handles top-level redirect\n * returns: in the embedded and popup flows the code arrives by\n * `postMessage` and never appears in this document's URL, so nothing\n * automatic can see it. No `setSession` branch — PKCE never yields raw\n * tokens (see `AuthResult`).\n */\n async function completeWithResult(\n result: AuthResult,\n priorUserId: string | null = null,\n ): Promise<boolean> {\n const { error } = await supabase.auth.exchangeCodeForSession(result.code)\n if (error) {\n // A failed exchange can still be success: the callback document spent\n // the code first. Adopt only a session that differs from `priorUserId`.\n // Same-user tenant rebind: no discriminator → fail closed.\n const sessionUserId = await getSessionUserId()\n if (sessionUserId === null) return false\n if (priorUserId !== null && sessionUserId === priorUserId) return false\n }\n const host = getHostPrincipal?.()\n if (host) writeBoundMarker(host)\n return true\n }\n\n function runHiddenAuthFrame(url: string): Promise<AuthResult | null> {\n return new Promise((resolve) => {\n const expectedOrigin = window.location.origin\n let settled = false\n const iframe = document.createElement('iframe')\n iframe.style.display = 'none'\n iframe.setAttribute('aria-hidden', 'true')\n const cleanup = () => {\n window.removeEventListener('message', onMessage)\n window.clearTimeout(timer)\n if (iframe.parentNode) iframe.parentNode.removeChild(iframe)\n }\n const finish = (result: AuthResult | null) => {\n if (settled) return\n settled = true\n cleanup()\n resolve(result)\n }\n const onMessage = (event: MessageEvent) => {\n if (event.origin !== expectedOrigin) return // trust only our own callback\n // ... and only OUR iframe: a concurrently-open sign-in popup posts the\n // same message type to this same window at the same origin, and\n // adopting its single-use code here would steal the exchange from the\n // interactive flow (and hand this silent flow a code it didn't earn).\n if (event.source !== iframe.contentWindow) return\n const data = event.data as CallbackMessage\n if (!data || data.type !== RESULT_MESSAGE_TYPE) return\n finish(parseCallbackMessage(data))\n }\n const timer = window.setTimeout(() => finish(null), timeouts.silentIframeMs)\n window.addEventListener('message', onMessage)\n iframe.src = url\n document.body.appendChild(iframe)\n })\n }\n\n let inFlightSilentSignIn: Promise<boolean> | null = null\n\n // Deduped WITHIN this document: several callers (the gate, an\n // identity-switch handler, a post-handshake retry) can fire at once, and\n // each signInWithOAuth() mints a PKCE code_verifier. Sharing one attempt\n // keeps them from clobbering each other. This does NOT coordinate across\n // documents — that's what `ensureSession` below is for.\n function trySilentSignIn(): Promise<boolean> {\n if (inFlightSilentSignIn) return inFlightSilentSignIn\n // Capped as a whole (not just the iframe wait) so a lock holder's turn is\n // truly bounded — the lock-wait timeout's sizing depends on this cap\n // being real.\n //\n // The cap must be TERMINAL, not just a race: a `Promise.race` alone\n // leaves the losing flow running, and an orphaned flow that completes its\n // code exchange AFTER we reported false would land a session moments\n // after the caller acted on the failure (rebindSession would then\n // signOut that very session). `expired` is checked before the one call\n // that can create a session, so once the timer fires the orphan can no\n // longer produce one — its unspent code simply dies.\n let expired = false\n inFlightSilentSignIn = Promise.race([\n runTrySilentSignIn(() => expired),\n new Promise<boolean>((resolve) =>\n setTimeout(() => {\n expired = true\n resolve(false)\n }, timeouts.silentFlowCapMs),\n ),\n ]).finally(() => {\n inFlightSilentSignIn = null\n })\n return inFlightSilentSignIn\n }\n\n async function runTrySilentSignIn(isExpired: () => boolean): Promise<boolean> {\n if (typeof window === 'undefined') return false\n // Snapshot whose session (if any) exists BEFORE the attempt, so\n // `completeWithResult`'s adoption fallback can tell a freshly-minted\n // session from a leftover one.\n const priorUserId = await getSessionUserId()\n const redirectTo = `${window.location.origin}${callbackPath}`\n const { data, error } = await supabase.auth.signInWithOAuth({\n provider: provider as never,\n options: {\n redirectTo,\n scopes: 'openid profile email',\n skipBrowserRedirect: true, // gives us the URL instead of redirecting\n queryParams: { prompt: 'none' }, // provider returns a result or error, no UI\n },\n })\n if (error || !data?.url) return false\n\n const result = await runHiddenAuthFrame(data.url)\n if (!result) return false\n // Past the cap the caller has already acted on `false` — do NOT exchange,\n // or a session would appear after the failure was handled (see the cap\n // comment in `trySilentSignIn`). Skipping leaves the code unspent; harmless.\n if (isExpired()) return false\n const ok = await completeWithResult(result, priorUserId)\n // The cap can also fire DURING the exchange. Roll a too-late success back\n // so \"the cap returned false\" always means \"no session got created\" —\n // otherwise this stray session appears after the caller handled the\n // failure, the exact hazard the pre-exchange check exists for. The next\n // attempt re-establishes it silently; a session nothing is waiting for is\n // worth less than the invariant.\n if (ok && isExpired()) {\n await supabase.auth.signOut()\n return false\n }\n return ok\n }\n\n // `fallback` answers \"did someone else succeed on my behalf?\" when the lock\n // can't be used — it differs per caller (any session for ensureSession,\n // the bound-user marker for rebindSession), so it can't be hardcoded here.\n async function withSsoLock(\n fn: () => Promise<boolean>,\n fallback: () => Promise<boolean>,\n ): Promise<boolean> {\n const locks = (\n navigator as unknown as {\n locks?: {\n request: (\n name: string,\n options: { signal?: AbortSignal },\n fn: () => Promise<boolean>,\n ) => Promise<boolean>\n }\n }\n ).locks\n if (!locks?.request) {\n // No Web Locks (e.g. an opaque origin if the iframe lost\n // allow-same-origin): an unsynchronised attempt plus a re-check still\n // beats a false negative.\n if (await fn()) return true\n return fallback()\n }\n\n const controller = new AbortController()\n const timer = window.setTimeout(() => controller.abort(), timeouts.lockWaitMs)\n try {\n return await locks.request(lockName, { signal: controller.signal }, fn)\n } catch {\n // Aborted waiting on a wedged sibling — never block the UI on the lock.\n return fallback()\n } finally {\n window.clearTimeout(timer)\n }\n }\n\n /**\n * THE function the gate calls. Gets a session, coordinated across every\n * document and tab on this origin.\n *\n * Several copies of this app run at once — the host mounts its iframe more\n * than once per page view, and a user can open it in two tabs. Each copy\n * is a separate JS context sharing one origin's localStorage, and each\n * would otherwise run its own silent flow. Only one can win; historically\n * the losers either painted a sign-in screen over the winner's session or\n * signed the winner out.\n *\n * Web Locks is the fix, and it is the same primitive supabase-js uses\n * internally to serialise token refresh across tabs. Exactly one context\n * runs the flow; the rest wait and then observe the session it wrote.\n *\n * Do NOT hand-roll this with a localStorage mutex: a Web Lock is released\n * automatically when the holding document dies (refresh, navigation,\n * crash), whereas a storage-based lock survives and needs TTLs and reclaim\n * logic.\n *\n * Double-checked locking — check before taking the lock (fast path) and\n * again after acquiring it, because the previous holder may have just\n * succeeded on our behalf.\n */\n async function ensureSession(): Promise<boolean> {\n if (typeof window === 'undefined') return false\n if (await hasValidSession()) return true\n\n return withSsoLock(\n async () => {\n if (await waitForSiblingSession()) return true // a sibling already did it\n if (await trySilentSignIn()) return true // our turn\n return hasValidSession() // one landed concurrently\n },\n // Lock unavailable/aborted: a sibling may still have succeeded meanwhile.\n hasValidSession,\n )\n }\n\n /**\n * Fresh sign-in as a NEW host identity, coordinated across documents. This\n * is what an identity-switch handler (`wireHostAuth`) calls — NOT\n * `ensureSession`, whose \"a session exists → done\" fast path would accept\n * the PREVIOUS user's still-valid session.\n *\n * The cross-document problem is the same as on load, inverted: the host\n * pushes the identity switch to EVERY mounted copy of the app, and each\n * would run its own silent flow — clobbering each other's PKCE\n * `code_verifier` in shared localStorage, so at best one succeeds and the\n * rest wrongly conclude the switch failed. Same lock fixes it; what\n * changes is the fast path. A plain Supabase session can't serve as one —\n * it carries a Supabase user from the IdP federation, the host carries a\n * Nominal userId, and there is no mapping between them. So the lock WINNER\n * stamps the Nominal userId it just bound into localStorage, and waiters\n * treat \"marker matches AND a session exists\" as proof a sibling already\n * re-bound this identity.\n */\n async function rebindSession(principal: HostPrincipal): Promise<boolean> {\n if (typeof window === 'undefined') return false\n\n // A sibling that beat us (to the lock, or before it wedged) already\n // signed in as THIS identity. Two guards, both load-bearing:\n // - NOT bare `hasValidSession` — that alone is satisfied by the OLD\n // user's session, the exact bug the identity-switch handler warns\n // about.\n // - NOT `isBoundTo` (unexpiring) — a marker from a switch to this same\n // principal DAYS ago would make waiters skip the live switch. The host\n // delivers one switch to all documents within milliseconds, so only a\n // marker younger than the lock wait can mean \"a sibling handled THIS\n // switch\".\n const siblingRebound = async () => {\n const m = readBoundMarker()\n if (m?.userId !== principal.userId) return false\n if (m.tenant !== principal.tenant) return false\n if (Date.now() - (m.at ?? 0) >= timeouts.lockWaitMs) return false\n if (!(await waitForSiblingSession())) return false\n const confirmed = readBoundMarker()\n if (confirmed?.userId !== principal.userId) return false\n if (confirmed.tenant !== principal.tenant) return false\n return Date.now() - (confirmed.at ?? 0) < timeouts.lockWaitMs\n }\n\n return withSsoLock(\n async () => {\n if (await siblingRebound()) return true\n const ok = await trySilentSignIn()\n if (ok) writeBoundMarker(principal)\n return ok\n },\n // Reached on lock-wait abort — i.e. we gave up while a sibling may\n // STILL be mid-flow, about to succeed and write the marker. Returning\n // false here makes the caller signOut(), which would destroy that\n // near-complete session, so grant one grace period before concluding\n // failure.\n async () => {\n if (await siblingRebound()) return true\n await new Promise((r) => setTimeout(r, timeouts.silentFlowCapMs))\n return siblingRebound()\n },\n )\n }\n\n /** Call on host logout, next to signOut(), so a stale marker can't vouch for a dead session. */\n function clearBoundUser(): void {\n if (typeof window === 'undefined') return\n localStorage.removeItem(boundUserKey)\n }\n\n return {\n hasValidSession,\n trySilentSignIn,\n ensureSession,\n rebindSession,\n isBoundTo,\n clearBoundUser,\n completeWithResult,\n }\n}\n","// Lovable-preview support, ported from the pre-package skill code — module-scope\n// capture is correct here (not a factory concern) since it reads the URL\n// once at boot, before any router strips the query string. In-memory only:\n// never persisted, never logged, never sent to any backend.\nconst PREVIEW_HOST_SUFFIX = '.lovable.app'\nconst SANDBOX_HOST_SUFFIX = '.lovableproject.com'\n\nlet capturedToken: string | null = null\n\n/** Capture `__lovable_token` from the URL as early as possible (module import). */\nexport function capturePreviewToken(): void {\n if (typeof window === 'undefined') return\n if (!window.location.hostname.endsWith(PREVIEW_HOST_SUFFIX)) return\n if (capturedToken) return\n try {\n const token = new URLSearchParams(window.location.search).get('__lovable_token')\n if (token) capturedToken = token\n } catch {\n /* never affect app boot */\n }\n}\n\n/** Hard gate: priming may ONLY ever run on Lovable-hosted preview origins. */\nexport function isLovableHostedHost(): boolean {\n if (typeof window === 'undefined') return false\n const h = window.location.hostname\n return h.endsWith(PREVIEW_HOST_SUFFIX) || h.endsWith(SANDBOX_HOST_SUFFIX)\n}\n\n/** Priming URL, or null when the feature is inert (any non-Lovable host). */\nexport function getPreviewPrimingUrl(): string | null {\n if (!isLovableHostedHost()) return null\n if (window.location.hostname.endsWith(PREVIEW_HOST_SUFFIX) && capturedToken) {\n return `${window.location.origin}/?__lovable_token=${encodeURIComponent(capturedToken)}`\n }\n return `${window.location.origin}/` // sandbox: bare origin is enough\n}\n\ncapturePreviewToken() // module scope: before any router strips the query string\n","export enum SignInKind {\n Authenticated = 'authenticated',\n Redirecting = 'redirecting',\n Failed = 'failed',\n}\n\nexport enum SignInFailureReason {\n /** window.open returned null → the \"allow popups\" hint */\n PopupBlocked = 'popup-blocked',\n /** signInWithOAuth errored (or returned no URL) */\n OauthError = 'oauth-error',\n /** popup closed / timed out before a result arrived */\n PopupClosed = 'popup-closed',\n /** a code arrived but redeeming it produced no session */\n ExchangeFailed = 'exchange-failed',\n /** no window: SSR or a non-browser context */\n Unsupported = 'unsupported',\n /** signInInteractive threw; the error is logged to the console */\n Unexpected = 'unexpected',\n}\n\n/** A session exists; the gate can open. */\nexport interface SignInAuthenticated {\n kind: SignInKind.Authenticated\n}\n\n/**\n * Top-level (non-iframe) flow: this page is navigating to the IdP. Not a\n * failure — keep any \"opening sign-in…\" state; the document is about to unload.\n */\nexport interface SignInRedirecting {\n kind: SignInKind.Redirecting\n}\n\n/** The attempt is over; `reason` picks the user-facing message. */\nexport interface SignInFailed {\n kind: SignInKind.Failed\n reason: SignInFailureReason\n}\n\nexport type SignInResult = SignInAuthenticated | SignInRedirecting | SignInFailed\n","// The interactive fallback — ported from the pre-package\n// silent-supabase-oidc-login skill code. Uses\n// `completeWithResult`/`parseCallbackMessage`/`AuthResult` from silentAuth.ts:\n// private helpers shared within one auth instance, not part of the public surface.\nimport type { ResolvedVibeAuthConfig } from './config'\nimport { RESULT_MESSAGE_TYPE } from './authMessage'\nimport type { AuthResult, SilentAuth } from './silentAuth'\nimport { parseCallbackMessage } from './silentAuth'\nimport { getPreviewPrimingUrl } from './previewToken'\n\nimport { SignInKind, SignInFailureReason, type SignInResult } from './signInResult'\n\nexport interface InteractiveAuth {\n signInInteractive(): Promise<SignInResult>\n}\n\nexport function createInteractiveAuth(\n config: ResolvedVibeAuthConfig,\n silentAuth: Pick<SilentAuth, 'completeWithResult'>,\n): InteractiveAuth {\n const { supabase, provider, callbackPath, timeouts } = config\n\n /** One-shot, scoped to this attempt — no standing listener that could adopt a stray session.\n * Timeout is owned by the caller (whole-flow bound); closing the popup settles this. */\n function waitForPopupResult(popup: Window): Promise<AuthResult | null> {\n return new Promise((resolve) => {\n const expectedOrigin = window.location.origin\n let settled = false\n const finish = (result: AuthResult | null) => {\n if (settled) return\n settled = true\n window.removeEventListener('message', onMessage)\n window.clearInterval(closedPoll)\n resolve(result)\n }\n const onMessage = (event: MessageEvent) => {\n if (event.origin !== expectedOrigin) return // our own origin only\n if (event.source !== popup) return // and only the window we opened\n const data = event.data\n if (!data || data.type !== RESULT_MESSAGE_TYPE) return\n finish(parseCallbackMessage(data))\n }\n window.addEventListener('message', onMessage)\n const closedPoll = window.setInterval(() => {\n if (popup.closed) finish(null) // user abandoned / timed out\n }, 500)\n })\n }\n\n /**\n * Best-effort gate priming: navigate the popup to our own origin and poll\n * until it settles back there (the gate is a multi-hop redirect chain, so\n * a single 'load' event is not enough; reading popup.location throws\n * mid-hop → keep polling). Never blocks past the timeout, never throws.\n */\n function primePopupForPreview(popup: Window): Promise<void> {\n const url = getPreviewPrimingUrl()\n if (!url) return Promise.resolve()\n\n return new Promise((resolve) => {\n let settled = false\n const expectedOrigin = window.location.origin\n const finish = () => {\n if (settled) return\n settled = true\n window.clearInterval(poll)\n window.clearTimeout(timer)\n resolve()\n }\n const timer = window.setTimeout(finish, timeouts.previewPrimingMs)\n const poll = window.setInterval(() => {\n if (popup.closed) return finish()\n try {\n if (popup.location.origin !== expectedOrigin) return\n if (popup.document.readyState !== 'complete') return\n finish()\n } catch {\n /* cross-origin hop — keep polling */\n }\n }, timeouts.previewPrimingPollMs)\n try {\n popup.location.href = url\n } catch {\n finish()\n }\n })\n }\n\n /** Never rejects: every caller (incl. custom sign-in screens) branches on\n * `kind`, and a rejection would strand pending UI state — so a throw\n * anywhere in the flow (network blip, a popup navigation error) is logged\n * and reported as an ordinary failure. */\n async function signInInteractive(): Promise<SignInResult> {\n // The popup is opened deep inside attemptSignIn; hold a reference out\n // here so a throw after window.open doesn't orphan a blank window.\n const popupRef: { current: Window | null } = { current: null }\n try {\n return await attemptSignIn((p) => (popupRef.current = p))\n } catch (err) {\n console.error('[vibe-auth] signInInteractive failed unexpectedly:', err)\n popupRef.current?.close()\n return { kind: SignInKind.Failed, reason: SignInFailureReason.Unexpected }\n }\n }\n\n async function attemptSignIn(trackPopup: (popup: Window) => void): Promise<SignInResult> {\n if (typeof window === 'undefined')\n return { kind: SignInKind.Failed, reason: SignInFailureReason.Unsupported }\n if (window.self === window.top) {\n // Already top-level: an ordinary redirect is fine, nothing to hand\n // back. Redirect to the callback path like every other flow — it is\n // the ONE allowlisted redirect target (allowlists are path-scoped; a\n // bare-origin target would be rejected and Supabase would bounce to\n // its Site URL). With no opener and no parent, the route bounces the\n // result to `/` where `detectSessionInUrl` completes the exchange.\n const { error } = await supabase.auth.signInWithOAuth({\n provider: provider as never,\n options: {\n redirectTo: `${window.location.origin}${callbackPath}`,\n scopes: 'openid profile email',\n },\n })\n if (error) return { kind: SignInKind.Failed, reason: SignInFailureReason.OauthError }\n return { kind: SignInKind.Redirecting } // navigating away — nothing more to report\n }\n\n // Embedded. Open the popup SYNCHRONOUSLY, before any await — after an\n // await the click's user activation is spent and the popup blocker\n // silently eats it.\n //\n // Deliberately NOT 'noopener'/'noreferrer': the callback needs\n // `window.opener` to hand the result back, and both flags also make\n // window.open() return null. Accepted consequence: documents in the\n // popup (the IdP's own pages) can navigate this window, so nothing\n // arriving from it is trusted — the listener below checks origin AND\n // source.\n const popup = window.open('about:blank', 'nominal-signin', 'width=520,height=680')\n if (!popup) return { kind: SignInKind.Failed, reason: SignInFailureReason.PopupBlocked }\n trackPopup(popup)\n\n // One timer for the whole interactive flow (prime → OAuth → wait). On\n // expiry close the popup and settle even if signInWithOAuth is hung;\n // `expired` blocks a late code exchange the same way silentFlowCapMs does.\n let expired = false\n let flowTimer: number | undefined\n const timeout = new Promise<SignInResult>((resolve) => {\n flowTimer = window.setTimeout(() => {\n expired = true\n popup.close()\n resolve({ kind: SignInKind.Failed, reason: SignInFailureReason.PopupClosed })\n }, timeouts.popupMs)\n })\n\n const flow = (async (): Promise<SignInResult> => {\n // Lovable preview only (inert elsewhere): let the popup pass the preview\n // gate BEFORE OAuth, so the gate can't intercept the callback route later.\n await primePopupForPreview(popup).catch(() => undefined)\n if (expired || popup.closed)\n return { kind: SignInKind.Failed, reason: SignInFailureReason.PopupClosed }\n\n const { data, error } = await supabase.auth.signInWithOAuth({\n provider: provider as never,\n options: {\n redirectTo: `${window.location.origin}${callbackPath}`,\n scopes: 'openid profile email',\n skipBrowserRedirect: true, // we navigate the popup ourselves\n },\n })\n if (expired || popup.closed)\n return { kind: SignInKind.Failed, reason: SignInFailureReason.PopupClosed }\n if (error || !data?.url) {\n popup.close()\n return { kind: SignInKind.Failed, reason: SignInFailureReason.OauthError }\n }\n popup.location.href = data.url\n\n const result = await waitForPopupResult(popup)\n if (expired || !result)\n return { kind: SignInKind.Failed, reason: SignInFailureReason.PopupClosed }\n\n // Complete in THIS context, never in the popup: storage is partitioned\n // by top-level site, so the popup has neither the PKCE code_verifier\n // (for the code shape) nor a storage bucket the iframe could read a\n // session from.\n const ok = await silentAuth.completeWithResult(result)\n if (expired) {\n void supabase.auth.signOut().catch(() => {})\n return { kind: SignInKind.Failed, reason: SignInFailureReason.PopupClosed }\n }\n return ok\n ? { kind: SignInKind.Authenticated }\n : { kind: SignInKind.Failed, reason: SignInFailureReason.ExchangeFailed }\n })()\n\n try {\n return await Promise.race([flow, timeout])\n } finally {\n if (flowTimer !== undefined) window.clearTimeout(flowTimer)\n }\n }\n\n return { signInInteractive }\n}\n","// Ported from the pre-package silent-supabase-oidc-login skill code (see\n// that SKILL.md's git history). The callback route serves BOTH flows: it\n// loads inside the hidden iframe (silent) AND inside the sign-in popup\n// (interactive). In both cases it reads the OAuth result from its own URL,\n// hands it to whichever window started the flow, and renders nothing.\n//\n// This module has NO import of silentAuth.ts/interactive.ts/the app's\n// Supabase client, on purpose: importing the client would boot it in the\n// callback window, and with `detectSessionInUrl` (on by default) it would\n// consume and strip the URL before this file gets to read it.\nimport { useEffect } from 'react'\nimport { RESULT_MESSAGE_TYPE } from './authMessage'\n\n/**\n * Snapshot the callback URL and return a `SilentCallback` component closed\n * over that snapshot.\n *\n * MUST be called synchronously, at the top of the app's own auth-setup\n * module (i.e. as part of `createVibeAuth`, itself called at module scope —\n * never lazily or inside a hook). Module bodies run synchronously at import;\n * supabase-js's `detectSessionInUrl` strips the `#access_token` hash\n * asynchronously shortly after a client boots ANYWHERE in the page. Reading\n * `window.location` later (inside a React effect, or after some other\n * module's async work) races that strip — it wins on a fast machine with a\n * warm module cache and loses on a cold one, which presents as \"works on my\n * laptop, fails on my colleague's\".\n *\n * The `posted` once-latch is scoped to THIS call, not a bare module `let` —\n * unlike the skill's single-instance-per-app assumption, a factory-based\n * package must not leak state across multiple `createVibeAuth` calls (e.g.\n * in tests).\n */\nexport function createCallbackHandler(): { SilentCallback: () => null } {\n // Snapshot before supabase-js can strip the hash (see above).\n const INITIAL_SEARCH = typeof window !== 'undefined' ? window.location.search : ''\n const INITIAL_HASH = typeof window !== 'undefined' ? window.location.hash : ''\n\n // The route can remount (dev-server refresh re-hits the URL); post at most once.\n let posted = false\n\n function SilentCallback(): null {\n useEffect(() => {\n if (posted) return\n // `opener` when opened as the interactive popup; `parent` when loaded\n // in the hidden silent iframe. Neither → either a top-level redirect\n // return (the interactive flow's non-embedded branch) or a bare direct\n // visit.\n const target = window.opener ?? (window.parent !== window ? window.parent : null)\n if (!target) {\n // Top-level return: EVERY flow redirects here (one allowlist entry,\n // on purpose), but this route must stay free of the Supabase client\n // (see above) — so hand the untouched OAuth result to the root,\n // where the client boots and `detectSessionInUrl` completes the\n // exchange (the gate's RETURNING_FROM_OAUTH guard holds silent auth\n // off that mount). A bare direct visit (no code, no error) just goes\n // home.\n posted = true\n window.location.replace(`/${INITIAL_SEARCH}${INITIAL_HASH}`)\n return\n }\n posted = true\n\n const params = new URLSearchParams(INITIAL_SEARCH)\n const hash = new URLSearchParams(INITIAL_HASH.replace(/^#/, ''))\n const code = params.get('code')\n // Hash is read for ERRORS ONLY. Never relay access_token/refresh_token:\n // PKCE cannot produce them, so forwarding them would only be a way for\n // an implicit-flow regression to keep half-working silently.\n const error =\n params.get('error') ??\n params.get('error_description') ??\n hash.get('error') ??\n hash.get('error_description')\n\n target.postMessage(\n { type: RESULT_MESSAGE_TYPE, code, error: code ? null : error },\n window.location.origin, // never use '*'\n )\n // The popup has done its job; the hidden iframe is torn down by its caller.\n if (window.opener) window.close()\n }, [])\n return null\n }\n\n return { SilentCallback }\n}\n","// Ported from the pre-package silent-supabase-oidc-login skill code, with two\n// behavior fixes over it (each has a regression test in __tests__/AuthGate.test.tsx):\n//\n// 1. The onAuthStateChange listener may only CLOSE an OPEN gate\n// (authenticated -> unauthenticated), never downgrade 'checking'. Diagnosed\n// from a production HAR trace: the app boots holding a stale persisted\n// session; supabase-js's own boot-time refresh of that session gets a 400\n// (revoked/rotated refresh token) and fires SIGNED_OUT WHILE the silent\n// flow set off by `resolve()` below is still in flight. The skill's\n// listener treated any null-session event as authoritative and flipped\n// 'checking' -> 'unauthenticated' immediately — painting the sign-in\n// button for the few seconds until the silent flow actually lands, then\n// flipping back to authenticated. `resolve()` (via `ensureSession`) is the\n// SOLE authority for the initial verdict; a stale session dying is not\n// news the listener should act on before resolve() has had its say.\n// 2. `DefaultSignInScreen` surfaces a failure message when\n// `signInInteractive()` resolves { kind: SignInKind.Failed } — the skill's\n// SignInScreen was silent on a blocked popup, indistinguishable from a\n// dead button. A 'redirecting' result is NOT a failure: the top-level\n// flow is navigating this page to the IdP, so the button stays in its\n// pending state until the document unloads.\nimport { useEffect, useState, type ReactNode } from 'react'\nimport type { ResolvedVibeAuthConfig } from './config'\nimport type { SilentAuth } from './silentAuth'\nimport type { InteractiveAuth } from './interactive'\nimport { SignInKind, SignInFailureReason } from './signInResult'\nimport type { HostAuth } from './hostAuth'\nimport type { HostPrincipal } from './principal'\n\nenum GateStatus {\n Checking = 'checking',\n Authenticated = 'authenticated',\n Unauthenticated = 'unauthenticated',\n}\n\nexport interface AuthGateProps {\n children: ReactNode\n /** Override the default sign-in screen (branding). Defaults to `DefaultSignInScreen`. */\n signInScreen?: ReactNode\n /** Override the default full-screen loader. Defaults to a bare \"Fetching your data…\" spinner-less div. */\n loader?: ReactNode\n}\n\nexport interface AuthGateBundle {\n AuthGate: (props: AuthGateProps) => ReactNode\n DefaultSignInScreen: () => ReactNode\n}\n\n/**\n * MUST be called synchronously, at the top of the app's own auth-setup\n * module (as part of `createVibeAuth`) — same timing constraint as\n * `createCallbackHandler`: `RETURNING_FROM_OAUTH`/`IS_CALLBACK_PATH` must be\n * captured before any effect runs, and before supabase-js's\n * `detectSessionInUrl` can strip the URL.\n */\nexport function createAuthGate(\n config: ResolvedVibeAuthConfig,\n silentAuth: SilentAuth,\n interactiveAuth: InteractiveAuth,\n hostAuth?: HostAuth,\n): AuthGateBundle {\n const { supabase, callbackPath, timeouts } = config\n\n // Did a top-level interactive sign-in just redirect back here with an\n // OAuth result in the URL? Captured at FACTORY-CALL SCOPE for the same\n // reason as the callback handler: supabase-js's `detectSessionInUrl`\n // strips the code/hash asynchronously once the client boots, so reading\n // `window.location` inside an effect races it.\n // The `access_token` check is NOT support for implicit flow (PKCE is\n // mandatory) — it's a regression detector: if the client ever loses\n // `flowType: 'pkce'`, the provider returns tokens in the hash, and without\n // this check the gate would fire a silent attempt on that mount and\n // clobber the in-progress return.\n const RETURNING_FROM_OAUTH =\n typeof window !== 'undefined' &&\n (new URLSearchParams(window.location.search).has('code') ||\n window.location.hash.includes('access_token'))\n\n // Same idea: capture early, but don't let it affect the FIRST render's\n // JSX — see the isCallback/useState comment below for why.\n const IS_CALLBACK_PATH =\n typeof window !== 'undefined' && window.location.pathname === callbackPath\n\n function DefaultFullScreenLoader() {\n return (\n <div\n style={{ display: 'grid', placeItems: 'center', height: '100vh', fontFamily: 'sans-serif' }}\n >\n Fetching your data…\n </div>\n )\n }\n\n function DefaultSignInScreen() {\n const [pending, setPending] = useState(false)\n const [failure, setFailure] = useState<SignInFailureReason | null>(null)\n return (\n <div\n style={{ display: 'grid', placeItems: 'center', height: '100vh', fontFamily: 'sans-serif' }}\n >\n <div style={{ textAlign: 'center' }}>\n <button\n type=\"button\"\n disabled={pending}\n onClick={() => {\n setPending(true)\n setFailure(null)\n void interactiveAuth.signInInteractive().then((result) => {\n setFailure(result.kind === SignInKind.Failed ? result.reason : null)\n // Redirecting keeps `pending` — this page is unloading, and\n // re-enabling the button would invite a second, doomed click.\n if (result.kind !== SignInKind.Redirecting) setPending(false)\n })\n }}\n >\n {pending ? 'Opening sign-in…' : 'Sign in'}\n </button>\n {failure ? (\n <p style={{ fontSize: 12, marginTop: 8 }}>\n {failure === SignInFailureReason.PopupBlocked\n ? 'Your browser blocked the sign-in popup. Allow popups for this site and try again.'\n : \"Sign-in didn't complete. Please try again.\"}\n </p>\n ) : null}\n </div>\n </div>\n )\n }\n\n // Wrap the ENTIRE app: <AuthGate><App /></AuthGate>. The app is `children`,\n // so it never mounts until `authenticated`.\n function AuthGate({ children, signInScreen, loader }: AuthGateProps): ReactNode {\n // /silent-callback loads INSIDE the hidden iframe (silent) and inside\n // the sign-in popup (interactive), and must render UNGATED in both. If\n // the gate intercepts it, it can't postMessage its result back and the\n // flow hangs forever. (Cleanest is to mount the callback route OUTSIDE\n // the gate; this in-gate exemption is the safety net for when the gate\n // wraps the whole app.)\n //\n // Neither `isCallback` nor `status` may depend on `window` at their\n // INITIAL value: in an SSR app, the server always renders with no\n // `window`, so if the client's first hydration pass evaluated real\n // `window.location` here instead, server and client would render\n // different things on the very first paint — \"Hydration failed\"\n // pointing at this component. Both start at their server-safe default\n // and get corrected in an effect, one tick after mount (a harmless\n // second render, not a hydration mismatch).\n const [isCallback, setIsCallback] = useState(false)\n const [status, setStatus] = useState<GateStatus>(GateStatus.Checking)\n\n useEffect(() => {\n if (IS_CALLBACK_PATH) setIsCallback(true)\n }, [])\n\n useEffect(() => {\n // Guard on the FACTORY-SCOPE CONSTANT, never the `isCallback` state:\n // the state is corrected one tick AFTER mount, so on the first commit\n // — even on the callback route itself — it is still `false`. Gating on\n // it would run resolve() inside the callback page and kick off a\n // redundant sign-in attempt there, interfering with the exchange it is\n // supposed to be completing. Only the RENDERED OUTPUT needs the\n // deferred state (for hydration); the logic wants the\n // immediately-correct value.\n if (IS_CALLBACK_PATH) return // never gate the callback route\n let cancelled = false\n let oauthTimer: number | undefined\n // Set when we learn the host principal. `ensureSession` must not open\n // the gate after this is set — the leftover session is unverified.\n let hostPrincipal: HostPrincipal | undefined\n // Bumps on every principal apply so an older async completion cannot\n // reopen the gate after a newer rebind/logout/host transition.\n let applyGen = 0\n // Host-wired boot starts this read alongside the seed wait. The first\n // principal apply shares it instead of paying for getSession() afterward.\n let pendingSessionRead: { promise: Promise<boolean>; settled: boolean } | undefined\n // Live AUTH_CHANGED / late seed share applyPrincipal. Host-wired starts\n // false until resolve() is about to wait, then true so a burst seed\n // isn't dropped after the waiter unsubscribes.\n let acceptPrincipalUpdates = !hostAuth?.isHostWired()\n\n const applyPrincipal = async (principal: HostPrincipal): Promise<void> => {\n hostPrincipal = principal\n const gen = ++applyGen\n // Hide any prior tenant/user content before the async rebind lands.\n setStatus(GateStatus.Checking)\n const prefetchedSession = pendingSessionRead\n pendingSessionRead = undefined\n const inFlightPrefetch =\n prefetchedSession && !prefetchedSession.settled ? prefetchedSession : undefined\n let sessionOk = inFlightPrefetch\n ? await inFlightPrefetch.promise\n : await silentAuth.hasValidSession().catch(() => false)\n // Any read started before the host principal arrived can settle with a\n // stale true or false. Confirm its result once against current storage.\n if (inFlightPrefetch) {\n sessionOk = await silentAuth.hasValidSession().catch(() => false)\n }\n if (cancelled || gen !== applyGen) return\n if (sessionOk && silentAuth.isBoundTo(principal)) {\n setStatus(GateStatus.Authenticated)\n return\n }\n const ok = await silentAuth.rebindSession(principal).catch(() => false)\n if (cancelled || gen !== applyGen) return\n if (!ok) void supabase.auth.signOut().catch(() => {})\n setStatus(ok ? GateStatus.Authenticated : GateStatus.Unauthenticated)\n }\n\n /** Open only when the live session is still bound to the host principal. */\n const openIfBound = async (): Promise<void> => {\n if (cancelled) return\n if (hostAuth?.isHostWired() && !hostPrincipal) return\n const gen = applyGen\n const principal = hostPrincipal\n if (principal) {\n const sessionOk = await silentAuth.hasValidSession().catch(() => false)\n if (cancelled || gen !== applyGen) return\n if (!(sessionOk && silentAuth.isBoundTo(principal))) return\n }\n setStatus(GateStatus.Authenticated)\n }\n\n async function resolve(): Promise<void> {\n // A top-level interactive sign-in just landed back here with its\n // result in the URL. Do NOT start a silent attempt now: every\n // signInWithOAuth() call mints a FRESH PKCE code_verifier and\n // overwrites the stored one, which would invalidate the very code\n // being redeemed right now (`bad_code_verifier`, 400 on\n // /token?grant_type=pkce). Let supabase-js's `detectSessionInUrl`\n // finish the exchange; its SIGNED_IN event opens the gate via the\n // listener below. The timeout is the fallback for an exchange that\n // fails or never fires.\n //\n // This branch is checked FIRST, before any silent-signin attempt,\n // and must survive StrictMode's mount→cleanup→remount: each mount\n // reschedules its own timer, and cleanup clears only its own — so\n // the surviving (second) mount always ends up with a live timer.\n // Rescheduling a fresh timer per mount is harmless.\n if (RETURNING_FROM_OAUTH) {\n oauthTimer = window.setTimeout(() => {\n if (!cancelled)\n setStatus((s) => (s === GateStatus.Checking ? GateStatus.Unauthenticated : s))\n }, timeouts.oauthCompletionMs)\n return\n }\n\n // Host-wired: wait for seed, then open if the marker already matches\n // this `{userId, tenant}` — otherwise rebind behind the loader.\n // Seed from a parent of AuthGate; seeding inside children deadlocks.\n // Accept live updates BEFORE the wait so a burst seed after the\n // waiter unsubscribes still reaches applyPrincipal. If the\n // subscription already applied, skip the waiter's (possibly stale) value.\n // Always return: a leftover session is not a host identity.\n if (hostAuth?.isHostWired()) {\n acceptPrincipalUpdates = true\n const sessionRead = {\n promise: silentAuth.hasValidSession().catch(() => false),\n settled: false,\n }\n void sessionRead.promise.finally(() => {\n sessionRead.settled = true\n })\n pendingSessionRead = sessionRead\n const next = await hostAuth.waitForSeededPrincipal(timeouts.oauthCompletionMs)\n if (cancelled) return\n if (next && applyGen === 0) await applyPrincipal(next)\n if (pendingSessionRead === sessionRead) pendingSessionRead = undefined\n if (applyGen === 0) setStatus(GateStatus.Unauthenticated)\n return\n }\n\n if (await silentAuth.ensureSession().catch(() => false)) {\n if (!cancelled && !hostPrincipal) setStatus(GateStatus.Authenticated)\n return\n }\n\n // No signOut() on failure — a sibling may still hold a good session.\n if (!cancelled && !hostPrincipal) setStatus(GateStatus.Unauthenticated)\n }\n\n void resolve()\n\n // Live host principal changes (and late seed when wireHostAuth ran after\n // mount) share applyPrincipal with the initial seed path above.\n const unsubPrincipal = hostAuth?.onSeededPrincipal((next) => {\n if (cancelled || !acceptPrincipalUpdates) return\n void applyPrincipal(next)\n })\n\n // A sibling document or another tab can establish a session AFTER\n // we've gated. `storage` fires in OTHER documents of this origin when\n // localStorage changes, and is the only cross-document signal\n // available — supabase's onAuthStateChange does not cross documents.\n // Without this, a late winner leaves this copy showing the sign-in\n // screen until reload. Removals stay closed; SIGNED_OUT closes an open gate.\n const onStorage = (e: StorageEvent) => {\n if (cancelled) return\n if (!e.key?.endsWith('-auth-token')) return\n pendingSessionRead = undefined\n if (!e.newValue) return\n void openIfBound()\n }\n window.addEventListener('storage', onStorage)\n\n // Re-close the gate on sign-out; open it once a manual sign-in\n // completes. IGNORE `INITIAL_SESSION`: it fires on mount carrying any\n // *persisted* Supabase session, and acting on it would open the gate\n // before `resolve()` re-validates live SSO — the exact stale-session\n // trust this package forbids. `resolve()` is the sole authority for\n // the initial open.\n //\n // v1 FIX (see file header): a null session may only CLOSE an OPEN\n // gate, never downgrade `checking`. `resolve()` above is the sole\n // authority for the FIRST verdict — a stale session's boot-time\n // refresh failing (SIGNED_OUT) is not a reason to preempt it.\n const { data: sub } = supabase.auth.onAuthStateChange((event, session) => {\n if (cancelled || event === 'INITIAL_SESSION') return\n if (session) void openIfBound()\n else {\n pendingSessionRead = undefined\n setStatus((s) => (s === GateStatus.Authenticated ? GateStatus.Unauthenticated : s))\n }\n })\n\n return () => {\n cancelled = true\n unsubPrincipal?.()\n if (oauthTimer !== undefined) window.clearTimeout(oauthTimer)\n sub.subscription.unsubscribe()\n window.removeEventListener('storage', onStorage)\n }\n }, []) // IS_CALLBACK_PATH is a stable constant, so this runs once\n\n if (isCallback) return <>{children}</> // callback route renders ungated\n if (status === GateStatus.Checking) return loader ?? <DefaultFullScreenLoader />\n if (status === GateStatus.Unauthenticated) return signInScreen ?? <DefaultSignInScreen /> // the ONLY login UI\n return <>{children}</>\n }\n\n return { AuthGate, DefaultSignInScreen }\n}\n","// Host logout & identity-switch handling, ported from the pre-package\n// silent-supabase-oidc-login skill code.\nimport type { ResolvedVibeAuthConfig } from './config'\nimport type { HostPrincipal } from './principal'\nimport type { SilentAuth } from './silentAuth'\n\n/** The subset of `VibeAppBridge` this module needs — structural, not a hard dependency. */\nexport interface AuthBridgeLike {\n onAuthChange(\n cb: (auth: { authenticated: boolean; userId?: string; tenant?: string }) => void,\n ): () => void\n}\n\nexport interface HostAuth {\n isHostWired(): boolean\n waitForSeededPrincipal(timeoutMs: number): Promise<HostPrincipal | null>\n onSeededPrincipal(cb: (principal: HostPrincipal) => void): () => void\n /**\n * Wire BEFORE `bridge.connect()`, at module scope so `AuthGate` sees\n * `isHostWired()` on first resolve.\n */\n wireHostAuth(bridge: AuthBridgeLike): () => void\n /**\n * Seed `{userId, tenant}` from `connect()`, from a **parent** of `AuthGate`.\n */\n seedLastUserId(userId: string, tenant: string): void\n /** Current seeded host `{userId, tenant}`, if any. */\n getPrincipal(): HostPrincipal | undefined\n}\n\nexport function createHostAuth(\n config: ResolvedVibeAuthConfig,\n silentAuth: Pick<SilentAuth, 'clearBoundUser'>,\n): HostAuth {\n const { supabase } = config\n\n let principal: HostPrincipal | undefined\n let hostWired = false\n const seedListeners = new Set<(next: HostPrincipal) => void>()\n\n function isHostWired(): boolean {\n return hostWired\n }\n\n function onSeededPrincipal(cb: (next: HostPrincipal) => void): () => void {\n seedListeners.add(cb)\n return () => {\n seedListeners.delete(cb)\n }\n }\n\n function waitForSeededPrincipal(timeoutMs: number): Promise<HostPrincipal | null> {\n if (principal) return Promise.resolve(principal)\n return new Promise((resolve) => {\n let settled = false\n const finish = (value: HostPrincipal | null) => {\n if (settled) return\n settled = true\n window.clearTimeout(timer)\n off()\n resolve(value)\n }\n const timer = window.setTimeout(() => finish(null), timeoutMs)\n const off = onSeededPrincipal((next) => finish(next))\n if (principal) finish(principal)\n })\n }\n\n function seedLastUserId(userId: string, tenant: string): void {\n principal = { userId, tenant }\n for (const listener of [...seedListeners]) listener(principal)\n }\n\n function wireHostAuth(bridge: AuthBridgeLike): () => void {\n hostWired = true\n return bridge.onAuthChange((auth) => {\n if (!auth.authenticated) {\n principal = undefined\n silentAuth.clearBoundUser()\n void supabase.auth.signOut().catch(() => {})\n return\n }\n if (!auth.userId) return\n const tenant = auth.tenant ?? principal?.tenant\n if (!tenant) {\n console.warn('[vibe-auth] AUTH_CHANGED authenticated without tenant; ignoring')\n return\n }\n if (principal?.userId === auth.userId && principal.tenant === tenant) return\n // Same path as seedLastUserId: AuthGate.applyPrincipal owns rebind/UI.\n // Notifying here (not rebinding inline) hides stale content behind the loader.\n seedLastUserId(auth.userId, tenant)\n })\n }\n\n return {\n wireHostAuth,\n seedLastUserId,\n getPrincipal: () => principal,\n isHostWired,\n waitForSeededPrincipal,\n onSeededPrincipal,\n }\n}\n","import { resolveConfig, assertPkce, type VibeAuthConfig } from './config'\nimport { createSilentAuth } from './silentAuth'\nimport { createInteractiveAuth } from './interactive'\nimport { createCallbackHandler } from './SilentCallback'\nimport { createAuthGate } from './AuthGate'\nimport { createHostAuth, type AuthBridgeLike, type HostAuth } from './hostAuth'\nimport type { HostPrincipal } from './principal'\nimport type { SignInResult } from './signInResult'\nimport { RESULT_MESSAGE_TYPE } from './authMessage'\n\nexport interface VibeAuth {\n // core\n /** True when a Supabase session already exists locally. */\n hasValidSession(): Promise<boolean>\n /**\n * Fresh sign-in as a NEW host identity, coordinated across documents. Call\n * this from your host-auth handler on an identity switch — NOT `ensureSession`.\n * Prefer `wireHostAuth`, which already calls this correctly.\n */\n rebindSession(principal: HostPrincipal): Promise<boolean>\n /**\n * Runs the `prompt=none` silent flow once, deduped within this document.\n * Most apps want `ensureSession` (the gate calls it); this is exposed for\n * advanced cases like a post-handshake retry.\n */\n trySilentSignIn(): Promise<boolean>\n /** Gets a session, coordinated across every document and tab on this origin. */\n ensureSession(): Promise<boolean>\n /**\n * The interactive (popup / top-level redirect) fallback. `AuthGate`'s\n * default sign-in screen calls this. Note `redirecting` is not a failure:\n * the page is navigating to the IdP and is about to unload.\n */\n signInInteractive(): Promise<SignInResult>\n /** Remove the identity-switch bound-user marker. Call on host logout, next to signOut(). */\n clearBoundUser(): void\n\n // react\n /** Wrap the ENTIRE app: `<AuthGate><App /></AuthGate>`. The app never mounts until authenticated. */\n AuthGate: ReturnType<typeof createAuthGate>['AuthGate']\n /** The default sign-in screen `AuthGate` renders unless you pass `signInScreen`. */\n DefaultSignInScreen: ReturnType<typeof createAuthGate>['DefaultSignInScreen']\n /** Mount this, and ONLY this, at `callbackPath` — ungated, outside `AuthGate`. */\n SilentCallback: ReturnType<typeof createCallbackHandler>['SilentCallback']\n\n // bridge integration\n /**\n * Wire the host's auth signal to this Supabase client. Call at module\n * scope, BEFORE `bridge.connect()`, so `AuthGate` will not open on a\n * leftover Supabase session before it knows the current Nominal user.\n * Returns the unsubscribe function.\n */\n wireHostAuth(bridge: AuthBridgeLike): () => void\n /**\n * Seed `{userId, tenant}` from `connect()`. Call from a **parent** of\n * `AuthGate` after `connect()` resolves. Start `connect()` at module scope,\n * not in a React effect, so the gate receives the live host principal as\n * early as possible. It then opens without rebind when the bound-user\n * marker already matches.\n */\n seedLastUserId(userId: string, tenant: string): void\n\n // constants\n /** The postMessage type relayed by `SilentCallback` — do not change if migrating an existing app. */\n RESULT_MESSAGE_TYPE: string\n /** The resolved callback path (default `/silent-callback`). */\n callbackPath: string\n}\n\n/**\n * Creates a configured, self-contained silent-SSO auth instance for a Nominal\n * Vibe App — the packaged, fixed replacement for a hand-copied\n * `silentAuth.ts`/`AuthGate.tsx` generated from the silent-supabase-oidc-login\n * skill.\n *\n * MUST be called synchronously, at the top of the app's own auth-setup\n * module (e.g. `src/lib/auth.ts`), and that module must be imported eagerly\n * (never lazily) — `AuthGate`/`SilentCallback` capture the callback URL at\n * this call, before any React effect or async work can let supabase-js's\n * `detectSessionInUrl` strip it first.\n *\n * @example\n * ```ts\n * // src/lib/auth.ts\n * import { createVibeAuth } from '@nominalso/vibe-auth'\n * import { supabase } from './supabaseClient'\n *\n * export const auth = createVibeAuth({\n * supabase,\n * provider: 'custom:supabase-fedapp', // from your Supabase Auth → Providers config\n * })\n * ```\n *\n * ```tsx\n * // src/routes/silent-callback.tsx (TanStack Start; ssr: false on this route)\n * import { auth } from '@/lib/auth'\n * export default auth.SilentCallback\n * ```\n *\n * ```tsx\n * // wrap the app root — see AGENTS.md for the SSR-safe recipe\n * <auth.AuthGate>\n * <App />\n * </auth.AuthGate>\n * ```\n *\n * ```ts\n * // Start the host handshake at module scope, before React mounts.\n * const unsub = auth.wireHostAuth(bridge)\n * export const hostContext = bridge.connect().then((ctx) => {\n * auth.seedLastUserId(ctx.user.id, ctx.tenant)\n * return ctx\n * })\n * ```\n */\nexport function createVibeAuth(userConfig: VibeAuthConfig): VibeAuth {\n const config = resolveConfig(userConfig)\n assertPkce(config.supabase)\n\n let hostAuth: HostAuth\n const silentAuth = createSilentAuth(config, () => hostAuth.getPrincipal())\n const interactiveAuth = createInteractiveAuth(config, silentAuth)\n const { SilentCallback } = createCallbackHandler()\n hostAuth = createHostAuth(config, silentAuth)\n const { AuthGate, DefaultSignInScreen } = createAuthGate(\n config,\n silentAuth,\n interactiveAuth,\n hostAuth,\n )\n const { wireHostAuth, seedLastUserId } = hostAuth\n\n return {\n hasValidSession: silentAuth.hasValidSession,\n rebindSession: silentAuth.rebindSession,\n trySilentSignIn: silentAuth.trySilentSignIn,\n ensureSession: silentAuth.ensureSession,\n signInInteractive: interactiveAuth.signInInteractive,\n clearBoundUser: silentAuth.clearBoundUser,\n AuthGate,\n DefaultSignInScreen,\n SilentCallback,\n wireHostAuth,\n seedLastUserId,\n RESULT_MESSAGE_TYPE,\n callbackPath: config.callbackPath,\n }\n}\n"],"mappings":"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","names":["AuthResultKind","SignInKind","SignInFailureReason","import_react"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/config.ts","../src/authMessage.ts","../src/silentAuth.ts","../src/previewToken.ts","../src/signInResult.ts","../src/interactive.ts","../src/SilentCallback.tsx","../src/AuthGate.tsx","../src/hostAuth.ts","../src/wireVibeApp.ts","../src/createVibeAuth.ts"],"sourcesContent":["export { createVibeAuth, type VibeAuth } from './createVibeAuth'\nexport type {\n VibeAuthConfig,\n VibeAuthTimeouts,\n ResolvedVibeAuthConfig,\n ResolvedVibeAuthTimeouts,\n} from './config'\nexport type { AuthGateProps } from './AuthGate'\nexport type { AuthBridgeLike } from './hostAuth'\nexport type {\n HostContextLike,\n ResetPayloadLike,\n VibeAppWiring,\n VibeBridgeLike,\n WireVibeAppOptions,\n} from './vibeAppWiring'\nexport type { HostPrincipal } from './principal'\nexport { AuthResultKind, type AuthResult } from './silentAuth'\nexport {\n SignInKind,\n SignInFailureReason,\n type SignInResult,\n type SignInAuthenticated,\n type SignInRedirecting,\n type SignInFailed,\n} from './signInResult'\nexport { RESULT_MESSAGE_TYPE } from './authMessage'\n","import type { SupabaseClient } from '@supabase/supabase-js'\n\n/**\n * Timeout knobs for the silent-SSO flow. All optional — sane defaults (below)\n * are the same values the silent-supabase-oidc-login skill shipped after its\n * production incidents, so only override these if you've measured a reason to.\n */\nexport interface VibeAuthTimeouts {\n /**\n * Bounds the hidden-iframe wait for a `prompt=none` result. Does NOT bound\n * the whole silent attempt — see `silentFlowCapMs`.\n * @default 10_000\n */\n silentIframeMs?: number\n /**\n * Hard cap on ONE WHOLE silent attempt (the `signInWithOAuth` fetch, the\n * hidden-iframe wait, and the code exchange). Without this, a slow attempt\n * could hold the cross-document lock past every waiter's patience.\n * @default 15_000\n */\n silentFlowCapMs?: number\n /**\n * How long a document queues for the cross-document Web Lock before giving\n * up. Sized off `silentFlowCapMs` by default (two full holder turns plus\n * margin) — pass this explicitly only if you also override `silentFlowCapMs`\n * and want the derived relationship preserved.\n * @default 2 * silentFlowCapMs + 5_000\n */\n lockWaitMs?: number\n /**\n * Bounds the wait for `detectSessionInUrl` to finish a top-level OAuth\n * return before the gate falls through to the sign-in screen.\n * @default 15_000\n */\n oauthCompletionMs?: number\n /** Bounds the interactive popup flow (user-facing — generous on purpose). @default 120_000 */\n popupMs?: number\n /** Bounds the Lovable-preview \"prime the popup\" navigation. @default 8_000 */\n previewPrimingMs?: number\n /** Poll interval while waiting for the preview-priming navigation to settle. @default 250 */\n previewPrimingPollMs?: number\n}\n\nexport interface ResolvedVibeAuthTimeouts {\n silentIframeMs: number\n silentFlowCapMs: number\n lockWaitMs: number\n oauthCompletionMs: number\n popupMs: number\n previewPrimingMs: number\n previewPrimingPollMs: number\n}\n\nexport const DEFAULT_TIMEOUTS = {\n silentIframeMs: 10_000,\n silentFlowCapMs: 15_000,\n oauthCompletionMs: 15_000,\n popupMs: 120_000,\n previewPrimingMs: 8_000,\n previewPrimingPollMs: 250,\n} as const\n\n/** Config passed to `createVibeAuth`. */\nexport interface VibeAuthConfig {\n /**\n * The app's own Supabase client — injected, never created by this package.\n * MUST have `auth.flowType: 'pkce'` (checked at init; see `assertPkce`). Own\n * this client for the same reason a generated app owns its `client.ts`: it\n * carries app-specific types, storage adapters, and Lovable Cloud wiring.\n */\n supabase: SupabaseClient\n /**\n * The IdP-federation provider name for `signInWithOAuth`, e.g.\n * `'custom:supabase-fedapp'`. Never guess this — it comes from the app's own\n * Supabase Auth → Providers configuration.\n */\n provider: string\n /**\n * Path of the same-origin OAuth callback route. Must match the app's actual\n * route AND the `redirect_to` the app's Supabase project allowlists.\n * @default '/silent-callback'\n */\n callbackPath?: string\n timeouts?: VibeAuthTimeouts\n /**\n * Prefix for the cross-document Web Lock name and the identity-switch\n * bound-user localStorage key. Only change this if multiple vibe apps with\n * DIFFERENT Supabase projects share one origin (rare) and you need their\n * locks/markers to not collide — same-project apps on different origins\n * never collide regardless, since Web Locks and localStorage are\n * origin-scoped.\n * @default 'nominal'\n */\n storageKeyPrefix?: string\n}\n\nexport interface ResolvedVibeAuthConfig {\n supabase: SupabaseClient\n provider: string\n callbackPath: string\n timeouts: ResolvedVibeAuthTimeouts\n lockName: string\n boundUserKey: string\n}\n\nexport function resolveConfig(config: VibeAuthConfig): ResolvedVibeAuthConfig {\n const callbackPath = config.callbackPath ?? '/silent-callback'\n const prefix = config.storageKeyPrefix ?? 'nominal'\n\n const silentFlowCapMs = config.timeouts?.silentFlowCapMs ?? DEFAULT_TIMEOUTS.silentFlowCapMs\n const lockWaitMs = config.timeouts?.lockWaitMs ?? 2 * silentFlowCapMs + 5_000\n\n return {\n supabase: config.supabase,\n provider: config.provider,\n callbackPath,\n timeouts: {\n silentIframeMs: config.timeouts?.silentIframeMs ?? DEFAULT_TIMEOUTS.silentIframeMs,\n silentFlowCapMs,\n lockWaitMs,\n oauthCompletionMs: config.timeouts?.oauthCompletionMs ?? DEFAULT_TIMEOUTS.oauthCompletionMs,\n popupMs: config.timeouts?.popupMs ?? DEFAULT_TIMEOUTS.popupMs,\n previewPrimingMs: config.timeouts?.previewPrimingMs ?? DEFAULT_TIMEOUTS.previewPrimingMs,\n previewPrimingPollMs:\n config.timeouts?.previewPrimingPollMs ?? DEFAULT_TIMEOUTS.previewPrimingPollMs,\n },\n lockName: `${prefix}-silent-sso`,\n boundUserKey: `${prefix}-sso-bound-user`,\n }\n}\n\n/**\n * Best-effort PKCE check. `flowType` isn't in supabase-js's public types (it's\n * an internal client option), so this reads it defensively and never throws —\n * a failed check must not break app boot, only warn loudly. This is the\n * runtime version of the skill's biggest \"verify this manually\" marker.\n */\nexport function assertPkce(supabase: SupabaseClient): void {\n try {\n const flowType = (supabase.auth as unknown as { flowType?: string }).flowType\n if (flowType !== 'pkce') {\n console.error(\n `[vibe-auth] Supabase client is not configured with { auth: { flowType: 'pkce' } } ` +\n `(got: ${String(flowType)}). This is a security control, not a preference — with ` +\n 'implicit flow the OAuth redirect carries a live access_token/refresh_token in the ' +\n \"URL, so an attacker-controlled redirect target yields a full session. Fix the app's \" +\n 'createClient(...) call before shipping.',\n )\n }\n } catch {\n // Never let a defensive check break app boot.\n }\n}\n","// No imports, on purpose. The callback route must not transitively import the\n// Supabase client — importing it boots the client in the callback window, and\n// with `detectSessionInUrl` (on by default) it will consume and strip the URL.\n//\n// This exact string is wire-compatible with every app generated from the\n// silent-supabase-oidc-login skill before this package existed — do not change\n// it; a migrated app must keep working alongside a not-yet-migrated sibling\n// document (hidden iframe, popup) on the same origin.\nexport const RESULT_MESSAGE_TYPE = 'silent-auth-result'\n","// Core of the silent-SSO flow — ported from the pre-package\n// silent-supabase-oidc-login skill code (see that SKILL.md's git history).\n// Module-scoped state in the skill (per-document; the\n// skill's apps are one document = one module graph) becomes factory-closure\n// state here for the same reason: `createVibeAuth` is called once per app,\n// so the closure IS the per-document scope.\nimport type { ResolvedVibeAuthConfig } from './config'\nimport type { HostPrincipal } from './principal'\nimport { RESULT_MESSAGE_TYPE } from './authMessage'\n\nconst SIBLING_SESSION_SYNC_MS = 50\n\nexport enum AuthResultKind {\n Code = 'code',\n}\n\n// ONE variant, on purpose. PKCE is mandatory (enforced by `assertPkce` at\n// init), so the only thing a callback can ever carry is an authorization\n// code. Do NOT add a `tokens` variant: that is implicit flow, and a token\n// payload can only mean the client config regressed — see\n// `parseCallbackMessage` below, which warns instead.\nexport type AuthResult = { kind: AuthResultKind.Code; code: string }\n\ntype CallbackMessage = {\n type?: string\n code?: string | null\n accessToken?: string | null\n refreshToken?: string | null\n error?: string | null\n} | null\n\n/** Normalize a postMessage payload from the callback route into an AuthResult. */\nexport function parseCallbackMessage(data: CallbackMessage): AuthResult | null {\n if (!data || data.type !== RESULT_MESSAGE_TYPE) return null\n if (data.code) return { kind: AuthResultKind.Code, code: data.code }\n if (data.accessToken || data.refreshToken) {\n // PKCE is enforced on the client, so the provider MUST return a code. Do\n // not \"helpfully\" accept tokens here — that silently re-enables implicit\n // flow. Warn loudly instead: it means the client config regressed.\n console.warn('[vibe-auth] ignoring implicit-flow token payload; PKCE expects ?code=')\n }\n return null\n}\n\nexport interface SilentAuth {\n hasValidSession(): Promise<boolean>\n trySilentSignIn(): Promise<boolean>\n ensureSession(): Promise<boolean>\n rebindSession(principal: HostPrincipal): Promise<boolean>\n /**\n * True when the bound-user marker is this host principal. No TTL — used by\n * `AuthGate` on load so a same-principal refresh can open without running\n * silent SSO. Live switch waiters still use a young-marker check inside\n * `rebindSession`.\n */\n isBoundTo(principal: HostPrincipal): boolean\n /**\n * Record that the live session belongs to this principal.\n *\n * Normally `completeWithResult` does this after redeeming a code. The\n * top-level redirect flow never routes through that function — the session\n * arrives via supabase-js's `detectSessionInUrl` — so `AuthGate` stamps it\n * there instead, and only for a `SIGNED_IN` it observed in that document.\n */\n stampBoundMarker(principal: HostPrincipal): void\n clearBoundUser(): void\n /** @internal exposed for interactive.ts, which shares completeWithResult/parseCallbackMessage's private helpers */\n completeWithResult(result: AuthResult): Promise<boolean>\n}\n\nexport function createSilentAuth(\n config: ResolvedVibeAuthConfig,\n // The host principal is the only identity we may stamp after THIS document\n // redeems a code — storage/SIGNED_IN must not stamp leftovers.\n getHostPrincipal?: () => HostPrincipal | undefined,\n // After stamp, re-seed so AuthGate.applyPrincipal re-checks (SIGNED_IN\n // often ran openIfBound before the marker existed).\n onBound?: (principal: HostPrincipal) => void,\n): SilentAuth {\n const { supabase, provider, callbackPath, timeouts, lockName, boundUserKey } = config\n\n /**\n * The current session's Supabase user id, or null when none exists.\n * `getSession()` already handles expiry (refreshing an expired session and\n * returning null when that fails), so \"non-null\" here means a live session.\n */\n async function getSessionUserId(): Promise<string | null> {\n const { data } = await supabase.auth.getSession()\n return data.session?.user?.id ?? null\n }\n\n /** True when a Supabase session already exists locally. */\n async function hasValidSession(): Promise<boolean> {\n return (await getSessionUserId()) !== null\n }\n\n // A Web Lock is released immediately after the winner stores its session,\n // but Supabase's BroadcastChannel update can reach a waiting client a task\n // later. One short grace check avoids minting a redundant PKCE flow.\n async function waitForSiblingSession(): Promise<boolean> {\n if (await hasValidSession()) return true\n await new Promise((resolve) => setTimeout(resolve, SIBLING_SESSION_SYNC_MS))\n return hasValidSession()\n }\n\n type BoundMarker = { userId?: string; tenant?: string; at?: number }\n\n function readBoundMarker(): BoundMarker | null {\n if (typeof window === 'undefined') return null\n try {\n return JSON.parse(localStorage.getItem(boundUserKey) ?? 'null') as BoundMarker | null\n } catch {\n return null\n }\n }\n\n function isBoundTo(principal: HostPrincipal): boolean {\n const m = readBoundMarker()\n return m?.userId === principal.userId && m?.tenant === principal.tenant\n }\n\n function writeBoundMarker({ userId, tenant }: HostPrincipal): void {\n if (typeof window === 'undefined') return\n localStorage.setItem(boundUserKey, JSON.stringify({ userId, tenant, at: Date.now() }))\n }\n\n /**\n * Redeem an authorization code relayed from the callback route.\n *\n * KEEP THIS even though `detectSessionInUrl` handles top-level redirect\n * returns: in the embedded and popup flows the code arrives by\n * `postMessage` and never appears in this document's URL, so nothing\n * automatic can see it. No `setSession` branch — PKCE never yields raw\n * tokens (see `AuthResult`).\n */\n async function completeWithResult(\n result: AuthResult,\n priorUserId: string | null = null,\n ): Promise<boolean> {\n const { error } = await supabase.auth.exchangeCodeForSession(result.code)\n if (error) {\n // A failed exchange can still be success: the callback document spent\n // the code first. Adopt only a session that differs from `priorUserId`.\n // Same-user tenant rebind: no discriminator → fail closed.\n const sessionUserId = await getSessionUserId()\n if (sessionUserId === null) return false\n if (priorUserId !== null && sessionUserId === priorUserId) return false\n }\n const host = getHostPrincipal?.()\n if (host) {\n writeBoundMarker(host)\n onBound?.(host)\n }\n return true\n }\n\n function runHiddenAuthFrame(url: string): Promise<AuthResult | null> {\n return new Promise((resolve) => {\n const expectedOrigin = window.location.origin\n let settled = false\n const iframe = document.createElement('iframe')\n iframe.style.display = 'none'\n iframe.setAttribute('aria-hidden', 'true')\n const cleanup = () => {\n window.removeEventListener('message', onMessage)\n window.clearTimeout(timer)\n if (iframe.parentNode) iframe.parentNode.removeChild(iframe)\n }\n const finish = (result: AuthResult | null) => {\n if (settled) return\n settled = true\n cleanup()\n resolve(result)\n }\n const onMessage = (event: MessageEvent) => {\n if (event.origin !== expectedOrigin) return // trust only our own callback\n // ... and only OUR iframe: a concurrently-open sign-in popup posts the\n // same message type to this same window at the same origin, and\n // adopting its single-use code here would steal the exchange from the\n // interactive flow (and hand this silent flow a code it didn't earn).\n if (event.source !== iframe.contentWindow) return\n const data = event.data as CallbackMessage\n if (!data || data.type !== RESULT_MESSAGE_TYPE) return\n finish(parseCallbackMessage(data))\n }\n const timer = window.setTimeout(() => finish(null), timeouts.silentIframeMs)\n window.addEventListener('message', onMessage)\n iframe.src = url\n document.body.appendChild(iframe)\n })\n }\n\n let inFlightSilentSignIn: Promise<boolean> | null = null\n\n // Deduped WITHIN this document: several callers (the gate, an\n // identity-switch handler, a post-handshake retry) can fire at once, and\n // each signInWithOAuth() mints a PKCE code_verifier. Sharing one attempt\n // keeps them from clobbering each other. This does NOT coordinate across\n // documents — that's what `ensureSession` below is for.\n function trySilentSignIn(): Promise<boolean> {\n if (inFlightSilentSignIn) return inFlightSilentSignIn\n // Capped as a whole (not just the iframe wait) so a lock holder's turn is\n // truly bounded — the lock-wait timeout's sizing depends on this cap\n // being real.\n //\n // The cap must be TERMINAL, not just a race: a `Promise.race` alone\n // leaves the losing flow running, and an orphaned flow that completes its\n // code exchange AFTER we reported false would land a session moments\n // after the caller acted on the failure (rebindSession would then\n // signOut that very session). `expired` is checked before the one call\n // that can create a session, so once the timer fires the orphan can no\n // longer produce one — its unspent code simply dies.\n let expired = false\n inFlightSilentSignIn = Promise.race([\n runTrySilentSignIn(() => expired),\n new Promise<boolean>((resolve) =>\n setTimeout(() => {\n expired = true\n resolve(false)\n }, timeouts.silentFlowCapMs),\n ),\n ]).finally(() => {\n inFlightSilentSignIn = null\n })\n return inFlightSilentSignIn\n }\n\n async function runTrySilentSignIn(isExpired: () => boolean): Promise<boolean> {\n if (typeof window === 'undefined') return false\n // Snapshot whose session (if any) exists BEFORE the attempt, so\n // `completeWithResult`'s adoption fallback can tell a freshly-minted\n // session from a leftover one.\n const priorUserId = await getSessionUserId()\n const redirectTo = `${window.location.origin}${callbackPath}`\n const { data, error } = await supabase.auth.signInWithOAuth({\n provider: provider as never,\n options: {\n redirectTo,\n scopes: 'openid profile email',\n skipBrowserRedirect: true, // gives us the URL instead of redirecting\n queryParams: { prompt: 'none' }, // provider returns a result or error, no UI\n },\n })\n if (error || !data?.url) return false\n\n const result = await runHiddenAuthFrame(data.url)\n if (!result) return false\n // Past the cap the caller has already acted on `false` — do NOT exchange,\n // or a session would appear after the failure was handled (see the cap\n // comment in `trySilentSignIn`). Skipping leaves the code unspent; harmless.\n if (isExpired()) return false\n const ok = await completeWithResult(result, priorUserId)\n // The cap can also fire DURING the exchange. Roll a too-late success back\n // so \"the cap returned false\" always means \"no session got created\" —\n // otherwise this stray session appears after the caller handled the\n // failure, the exact hazard the pre-exchange check exists for. The next\n // attempt re-establishes it silently; a session nothing is waiting for is\n // worth less than the invariant.\n if (ok && isExpired()) {\n await supabase.auth.signOut()\n return false\n }\n return ok\n }\n\n // `fallback` answers \"did someone else succeed on my behalf?\" when the lock\n // can't be used — it differs per caller (any session for ensureSession,\n // the bound-user marker for rebindSession), so it can't be hardcoded here.\n async function withSsoLock(\n fn: () => Promise<boolean>,\n fallback: () => Promise<boolean>,\n ): Promise<boolean> {\n const locks = (\n navigator as unknown as {\n locks?: {\n request: (\n name: string,\n options: { signal?: AbortSignal },\n fn: () => Promise<boolean>,\n ) => Promise<boolean>\n }\n }\n ).locks\n if (!locks?.request) {\n // No Web Locks (e.g. an opaque origin if the iframe lost\n // allow-same-origin): an unsynchronised attempt plus a re-check still\n // beats a false negative.\n if (await fn()) return true\n return fallback()\n }\n\n const controller = new AbortController()\n const timer = window.setTimeout(() => controller.abort(), timeouts.lockWaitMs)\n try {\n return await locks.request(lockName, { signal: controller.signal }, fn)\n } catch {\n // Aborted waiting on a wedged sibling — never block the UI on the lock.\n return fallback()\n } finally {\n window.clearTimeout(timer)\n }\n }\n\n /**\n * THE function the gate calls. Gets a session, coordinated across every\n * document and tab on this origin.\n *\n * Several copies of this app run at once — the host mounts its iframe more\n * than once per page view, and a user can open it in two tabs. Each copy\n * is a separate JS context sharing one origin's localStorage, and each\n * would otherwise run its own silent flow. Only one can win; historically\n * the losers either painted a sign-in screen over the winner's session or\n * signed the winner out.\n *\n * Web Locks is the fix, and it is the same primitive supabase-js uses\n * internally to serialise token refresh across tabs. Exactly one context\n * runs the flow; the rest wait and then observe the session it wrote.\n *\n * Do NOT hand-roll this with a localStorage mutex: a Web Lock is released\n * automatically when the holding document dies (refresh, navigation,\n * crash), whereas a storage-based lock survives and needs TTLs and reclaim\n * logic.\n *\n * Double-checked locking — check before taking the lock (fast path) and\n * again after acquiring it, because the previous holder may have just\n * succeeded on our behalf.\n */\n async function ensureSession(): Promise<boolean> {\n if (typeof window === 'undefined') return false\n if (await hasValidSession()) return true\n\n return withSsoLock(\n async () => {\n if (await waitForSiblingSession()) return true // a sibling already did it\n if (await trySilentSignIn()) return true // our turn\n return hasValidSession() // one landed concurrently\n },\n // Lock unavailable/aborted: a sibling may still have succeeded meanwhile.\n hasValidSession,\n )\n }\n\n /**\n * Fresh sign-in as a NEW host identity, coordinated across documents. This\n * is what an identity-switch handler (`wireHostAuth`) calls — NOT\n * `ensureSession`, whose \"a session exists → done\" fast path would accept\n * the PREVIOUS user's still-valid session.\n *\n * The cross-document problem is the same as on load, inverted: the host\n * pushes the identity switch to EVERY mounted copy of the app, and each\n * would run its own silent flow — clobbering each other's PKCE\n * `code_verifier` in shared localStorage, so at best one succeeds and the\n * rest wrongly conclude the switch failed. Same lock fixes it; what\n * changes is the fast path. A plain Supabase session can't serve as one —\n * it carries a Supabase user from the IdP federation, the host carries a\n * Nominal userId, and there is no mapping between them. So the lock WINNER\n * stamps the Nominal userId it just bound into localStorage, and waiters\n * treat \"marker matches AND a session exists\" as proof a sibling already\n * re-bound this identity.\n */\n async function rebindSession(principal: HostPrincipal): Promise<boolean> {\n if (typeof window === 'undefined') return false\n\n // A sibling that beat us (to the lock, or before it wedged) already\n // signed in as THIS identity. Two guards, both load-bearing:\n // - NOT bare `hasValidSession` — that alone is satisfied by the OLD\n // user's session, the exact bug the identity-switch handler warns\n // about.\n // - NOT `isBoundTo` (unexpiring) — a marker from a switch to this same\n // principal DAYS ago would make waiters skip the live switch. The host\n // delivers one switch to all documents within milliseconds, so only a\n // marker younger than the lock wait can mean \"a sibling handled THIS\n // switch\".\n const siblingRebound = async () => {\n const m = readBoundMarker()\n if (m?.userId !== principal.userId) return false\n if (m.tenant !== principal.tenant) return false\n if (Date.now() - (m.at ?? 0) >= timeouts.lockWaitMs) return false\n if (!(await waitForSiblingSession())) return false\n const confirmed = readBoundMarker()\n if (confirmed?.userId !== principal.userId) return false\n if (confirmed.tenant !== principal.tenant) return false\n return Date.now() - (confirmed.at ?? 0) < timeouts.lockWaitMs\n }\n\n return withSsoLock(\n async () => {\n if (await siblingRebound()) return true\n const ok = await trySilentSignIn()\n if (ok) writeBoundMarker(principal)\n return ok\n },\n // Reached on lock-wait abort — i.e. we gave up while a sibling may\n // STILL be mid-flow, about to succeed and write the marker. Returning\n // false here makes the caller signOut(), which would destroy that\n // near-complete session, so grant one grace period before concluding\n // failure.\n async () => {\n if (await siblingRebound()) return true\n await new Promise((r) => setTimeout(r, timeouts.silentFlowCapMs))\n return siblingRebound()\n },\n )\n }\n\n /** Call on host logout, next to signOut(), so a stale marker can't vouch for a dead session. */\n function clearBoundUser(): void {\n if (typeof window === 'undefined') return\n localStorage.removeItem(boundUserKey)\n }\n\n return {\n hasValidSession,\n trySilentSignIn,\n ensureSession,\n rebindSession,\n isBoundTo,\n stampBoundMarker: writeBoundMarker,\n clearBoundUser,\n completeWithResult,\n }\n}\n","// Lovable-preview support, ported from the pre-package skill code — module-scope\n// capture is correct here (not a factory concern) since it reads the URL\n// once at boot, before any router strips the query string. In-memory only:\n// never persisted, never logged, never sent to any backend.\nconst PREVIEW_HOST_SUFFIX = '.lovable.app'\nconst SANDBOX_HOST_SUFFIX = '.lovableproject.com'\n\nlet capturedToken: string | null = null\n\n/** Capture `__lovable_token` from the URL as early as possible (module import). */\nexport function capturePreviewToken(): void {\n if (typeof window === 'undefined') return\n if (!window.location.hostname.endsWith(PREVIEW_HOST_SUFFIX)) return\n if (capturedToken) return\n try {\n const token = new URLSearchParams(window.location.search).get('__lovable_token')\n if (token) capturedToken = token\n } catch {\n /* never affect app boot */\n }\n}\n\n/** Hard gate: priming may ONLY ever run on Lovable-hosted preview origins. */\nexport function isLovableHostedHost(): boolean {\n if (typeof window === 'undefined') return false\n const h = window.location.hostname\n return h.endsWith(PREVIEW_HOST_SUFFIX) || h.endsWith(SANDBOX_HOST_SUFFIX)\n}\n\n/** Priming URL, or null when the feature is inert (any non-Lovable host). */\nexport function getPreviewPrimingUrl(): string | null {\n if (!isLovableHostedHost()) return null\n if (window.location.hostname.endsWith(PREVIEW_HOST_SUFFIX) && capturedToken) {\n return `${window.location.origin}/?__lovable_token=${encodeURIComponent(capturedToken)}`\n }\n return `${window.location.origin}/` // sandbox: bare origin is enough\n}\n\ncapturePreviewToken() // module scope: before any router strips the query string\n","export enum SignInKind {\n Authenticated = 'authenticated',\n Redirecting = 'redirecting',\n Failed = 'failed',\n}\n\nexport enum SignInFailureReason {\n /** window.open returned null → the \"allow popups\" hint */\n PopupBlocked = 'popup-blocked',\n /** signInWithOAuth errored (or returned no URL) */\n OauthError = 'oauth-error',\n /** popup closed / timed out before a result arrived */\n PopupClosed = 'popup-closed',\n /** a code arrived but redeeming it produced no session */\n ExchangeFailed = 'exchange-failed',\n /** no window: SSR or a non-browser context */\n Unsupported = 'unsupported',\n /** signInInteractive threw; the error is logged to the console */\n Unexpected = 'unexpected',\n}\n\n/** A session exists; the gate can open. */\nexport interface SignInAuthenticated {\n kind: SignInKind.Authenticated\n}\n\n/**\n * Top-level (non-iframe) flow: this page is navigating to the IdP. Not a\n * failure — keep any \"opening sign-in…\" state; the document is about to unload.\n */\nexport interface SignInRedirecting {\n kind: SignInKind.Redirecting\n}\n\n/** The attempt is over; `reason` picks the user-facing message. */\nexport interface SignInFailed {\n kind: SignInKind.Failed\n reason: SignInFailureReason\n}\n\nexport type SignInResult = SignInAuthenticated | SignInRedirecting | SignInFailed\n","// The interactive fallback — ported from the pre-package\n// silent-supabase-oidc-login skill code. Uses\n// `completeWithResult`/`parseCallbackMessage`/`AuthResult` from silentAuth.ts:\n// private helpers shared within one auth instance, not part of the public surface.\nimport type { ResolvedVibeAuthConfig } from './config'\nimport { RESULT_MESSAGE_TYPE } from './authMessage'\nimport type { AuthResult, SilentAuth } from './silentAuth'\nimport { parseCallbackMessage } from './silentAuth'\nimport { getPreviewPrimingUrl } from './previewToken'\n\nimport { SignInKind, SignInFailureReason, type SignInResult } from './signInResult'\n\nexport interface InteractiveAuth {\n signInInteractive(): Promise<SignInResult>\n}\n\nexport function createInteractiveAuth(\n config: ResolvedVibeAuthConfig,\n silentAuth: Pick<SilentAuth, 'completeWithResult'>,\n): InteractiveAuth {\n const { supabase, provider, callbackPath, timeouts } = config\n\n /** One-shot, scoped to this attempt — no standing listener that could adopt a stray session.\n * Timeout is owned by the caller (whole-flow bound); closing the popup settles this. */\n function waitForPopupResult(popup: Window): Promise<AuthResult | null> {\n return new Promise((resolve) => {\n const expectedOrigin = window.location.origin\n let settled = false\n const finish = (result: AuthResult | null) => {\n if (settled) return\n settled = true\n window.removeEventListener('message', onMessage)\n window.clearInterval(closedPoll)\n resolve(result)\n }\n const onMessage = (event: MessageEvent) => {\n if (event.origin !== expectedOrigin) return // our own origin only\n if (event.source !== popup) return // and only the window we opened\n const data = event.data\n if (!data || data.type !== RESULT_MESSAGE_TYPE) return\n finish(parseCallbackMessage(data))\n }\n window.addEventListener('message', onMessage)\n const closedPoll = window.setInterval(() => {\n if (popup.closed) finish(null) // user abandoned / timed out\n }, 500)\n })\n }\n\n /**\n * Best-effort gate priming: navigate the popup to our own origin and poll\n * until it settles back there (the gate is a multi-hop redirect chain, so\n * a single 'load' event is not enough; reading popup.location throws\n * mid-hop → keep polling). Never blocks past the timeout, never throws.\n */\n function primePopupForPreview(popup: Window): Promise<void> {\n const url = getPreviewPrimingUrl()\n if (!url) return Promise.resolve()\n\n return new Promise((resolve) => {\n let settled = false\n const expectedOrigin = window.location.origin\n const finish = () => {\n if (settled) return\n settled = true\n window.clearInterval(poll)\n window.clearTimeout(timer)\n resolve()\n }\n const timer = window.setTimeout(finish, timeouts.previewPrimingMs)\n const poll = window.setInterval(() => {\n if (popup.closed) return finish()\n try {\n if (popup.location.origin !== expectedOrigin) return\n if (popup.document.readyState !== 'complete') return\n finish()\n } catch {\n /* cross-origin hop — keep polling */\n }\n }, timeouts.previewPrimingPollMs)\n try {\n popup.location.href = url\n } catch {\n finish()\n }\n })\n }\n\n /** Never rejects: every caller (incl. custom sign-in screens) branches on\n * `kind`, and a rejection would strand pending UI state — so a throw\n * anywhere in the flow (network blip, a popup navigation error) is logged\n * and reported as an ordinary failure. */\n async function signInInteractive(): Promise<SignInResult> {\n // The popup is opened deep inside attemptSignIn; hold a reference out\n // here so a throw after window.open doesn't orphan a blank window.\n const popupRef: { current: Window | null } = { current: null }\n try {\n return await attemptSignIn((p) => (popupRef.current = p))\n } catch (err) {\n console.error('[vibe-auth] signInInteractive failed unexpectedly:', err)\n popupRef.current?.close()\n return { kind: SignInKind.Failed, reason: SignInFailureReason.Unexpected }\n }\n }\n\n async function attemptSignIn(trackPopup: (popup: Window) => void): Promise<SignInResult> {\n if (typeof window === 'undefined')\n return { kind: SignInKind.Failed, reason: SignInFailureReason.Unsupported }\n if (window.self === window.top) {\n // Already top-level: an ordinary redirect is fine, nothing to hand\n // back. Redirect to the callback path like every other flow — it is\n // the ONE allowlisted redirect target (allowlists are path-scoped; a\n // bare-origin target would be rejected and Supabase would bounce to\n // its Site URL). With no opener and no parent, the route bounces the\n // result to `/` where `detectSessionInUrl` completes the exchange.\n const { error } = await supabase.auth.signInWithOAuth({\n provider: provider as never,\n options: {\n redirectTo: `${window.location.origin}${callbackPath}`,\n scopes: 'openid profile email',\n },\n })\n if (error) return { kind: SignInKind.Failed, reason: SignInFailureReason.OauthError }\n return { kind: SignInKind.Redirecting } // navigating away — nothing more to report\n }\n\n // Embedded. Open the popup SYNCHRONOUSLY, before any await — after an\n // await the click's user activation is spent and the popup blocker\n // silently eats it.\n //\n // Deliberately NOT 'noopener'/'noreferrer': the callback needs\n // `window.opener` to hand the result back, and both flags also make\n // window.open() return null. Accepted consequence: documents in the\n // popup (the IdP's own pages) can navigate this window, so nothing\n // arriving from it is trusted — the listener below checks origin AND\n // source.\n const popup = window.open('about:blank', 'nominal-signin', 'width=520,height=680')\n if (!popup) return { kind: SignInKind.Failed, reason: SignInFailureReason.PopupBlocked }\n trackPopup(popup)\n\n // One timer for the whole interactive flow (prime → OAuth → wait). On\n // expiry close the popup and settle even if signInWithOAuth is hung;\n // `expired` blocks a late code exchange the same way silentFlowCapMs does.\n let expired = false\n let flowTimer: number | undefined\n const timeout = new Promise<SignInResult>((resolve) => {\n flowTimer = window.setTimeout(() => {\n expired = true\n popup.close()\n resolve({ kind: SignInKind.Failed, reason: SignInFailureReason.PopupClosed })\n }, timeouts.popupMs)\n })\n\n const flow = (async (): Promise<SignInResult> => {\n // Lovable preview only (inert elsewhere): let the popup pass the preview\n // gate BEFORE OAuth, so the gate can't intercept the callback route later.\n await primePopupForPreview(popup).catch(() => undefined)\n if (expired || popup.closed)\n return { kind: SignInKind.Failed, reason: SignInFailureReason.PopupClosed }\n\n const { data, error } = await supabase.auth.signInWithOAuth({\n provider: provider as never,\n options: {\n redirectTo: `${window.location.origin}${callbackPath}`,\n scopes: 'openid profile email',\n skipBrowserRedirect: true, // we navigate the popup ourselves\n },\n })\n if (expired || popup.closed)\n return { kind: SignInKind.Failed, reason: SignInFailureReason.PopupClosed }\n if (error || !data?.url) {\n popup.close()\n return { kind: SignInKind.Failed, reason: SignInFailureReason.OauthError }\n }\n popup.location.href = data.url\n\n const result = await waitForPopupResult(popup)\n if (expired || !result)\n return { kind: SignInKind.Failed, reason: SignInFailureReason.PopupClosed }\n\n // Complete in THIS context, never in the popup: storage is partitioned\n // by top-level site, so the popup has neither the PKCE code_verifier\n // (for the code shape) nor a storage bucket the iframe could read a\n // session from.\n const ok = await silentAuth.completeWithResult(result)\n if (expired) {\n void supabase.auth.signOut().catch(() => {})\n return { kind: SignInKind.Failed, reason: SignInFailureReason.PopupClosed }\n }\n return ok\n ? { kind: SignInKind.Authenticated }\n : { kind: SignInKind.Failed, reason: SignInFailureReason.ExchangeFailed }\n })()\n\n try {\n return await Promise.race([flow, timeout])\n } finally {\n if (flowTimer !== undefined) window.clearTimeout(flowTimer)\n }\n }\n\n return { signInInteractive }\n}\n","// Ported from the pre-package silent-supabase-oidc-login skill code (see\n// that SKILL.md's git history). The callback route serves BOTH flows: it\n// loads inside the hidden iframe (silent) AND inside the sign-in popup\n// (interactive). In both cases it reads the OAuth result from its own URL,\n// hands it to whichever window started the flow, and renders nothing.\n//\n// This module has NO import of silentAuth.ts/interactive.ts/the app's\n// Supabase client, on purpose: importing the client would boot it in the\n// callback window, and with `detectSessionInUrl` (on by default) it would\n// consume and strip the URL before this file gets to read it.\nimport { useEffect } from 'react'\nimport { RESULT_MESSAGE_TYPE } from './authMessage'\n\n/**\n * Snapshot the callback URL and return a `SilentCallback` component closed\n * over that snapshot.\n *\n * MUST be called synchronously, at the top of the app's own auth-setup\n * module (i.e. as part of `createVibeAuth`, itself called at module scope —\n * never lazily or inside a hook). Module bodies run synchronously at import;\n * supabase-js's `detectSessionInUrl` strips the `#access_token` hash\n * asynchronously shortly after a client boots ANYWHERE in the page. Reading\n * `window.location` later (inside a React effect, or after some other\n * module's async work) races that strip — it wins on a fast machine with a\n * warm module cache and loses on a cold one, which presents as \"works on my\n * laptop, fails on my colleague's\".\n *\n * The `posted` once-latch is scoped to THIS call, not a bare module `let` —\n * unlike the skill's single-instance-per-app assumption, a factory-based\n * package must not leak state across multiple `createVibeAuth` calls (e.g.\n * in tests).\n */\nexport function createCallbackHandler(): { SilentCallback: () => null } {\n // Snapshot before supabase-js can strip the hash (see above).\n const INITIAL_SEARCH = typeof window !== 'undefined' ? window.location.search : ''\n const INITIAL_HASH = typeof window !== 'undefined' ? window.location.hash : ''\n\n // The route can remount (dev-server refresh re-hits the URL); post at most once.\n let posted = false\n\n function SilentCallback(): null {\n useEffect(() => {\n if (posted) return\n // `opener` when opened as the interactive popup; `parent` when loaded\n // in the hidden silent iframe. Neither → either a top-level redirect\n // return (the interactive flow's non-embedded branch) or a bare direct\n // visit.\n const target = window.opener ?? (window.parent !== window ? window.parent : null)\n if (!target) {\n // Top-level return: EVERY flow redirects here (one allowlist entry,\n // on purpose), but this route must stay free of the Supabase client\n // (see above) — so hand the untouched OAuth result to the root,\n // where the client boots and `detectSessionInUrl` completes the\n // exchange (the gate's RETURNING_FROM_OAUTH guard holds silent auth\n // off that mount). A bare direct visit (no code, no error) just goes\n // home.\n posted = true\n window.location.replace(`/${INITIAL_SEARCH}${INITIAL_HASH}`)\n return\n }\n posted = true\n\n const params = new URLSearchParams(INITIAL_SEARCH)\n const hash = new URLSearchParams(INITIAL_HASH.replace(/^#/, ''))\n const code = params.get('code')\n // Hash is read for ERRORS ONLY. Never relay access_token/refresh_token:\n // PKCE cannot produce them, so forwarding them would only be a way for\n // an implicit-flow regression to keep half-working silently.\n const error =\n params.get('error') ??\n params.get('error_description') ??\n hash.get('error') ??\n hash.get('error_description')\n\n target.postMessage(\n { type: RESULT_MESSAGE_TYPE, code, error: code ? null : error },\n window.location.origin, // never use '*'\n )\n // The popup has done its job; the hidden iframe is torn down by its caller.\n if (window.opener) window.close()\n }, [])\n return null\n }\n\n return { SilentCallback }\n}\n","// Ported from the pre-package silent-supabase-oidc-login skill code, with two\n// behavior fixes over it (each has a regression test in __tests__/AuthGate.test.tsx):\n//\n// 1. The onAuthStateChange listener may only CLOSE an OPEN gate\n// (authenticated -> unauthenticated), never downgrade 'checking'. Diagnosed\n// from a production HAR trace: the app boots holding a stale persisted\n// session; supabase-js's own boot-time refresh of that session gets a 400\n// (revoked/rotated refresh token) and fires SIGNED_OUT WHILE the silent\n// flow set off by `resolve()` below is still in flight. The skill's\n// listener treated any null-session event as authoritative and flipped\n// 'checking' -> 'unauthenticated' immediately — painting the sign-in\n// button for the few seconds until the silent flow actually lands, then\n// flipping back to authenticated. `resolve()` (via `ensureSession`) is the\n// SOLE authority for the initial verdict; a stale session dying is not\n// news the listener should act on before resolve() has had its say.\n// 2. `DefaultSignInScreen` surfaces a failure message when\n// `signInInteractive()` resolves { kind: SignInKind.Failed } — the skill's\n// SignInScreen was silent on a blocked popup, indistinguishable from a\n// dead button. A 'redirecting' result is NOT a failure: the top-level\n// flow is navigating this page to the IdP, so the button stays in its\n// pending state until the document unloads.\nimport { useEffect, useState, type ReactNode } from 'react'\nimport type { ResolvedVibeAuthConfig } from './config'\nimport type { SilentAuth } from './silentAuth'\nimport type { InteractiveAuth } from './interactive'\nimport { SignInKind, SignInFailureReason } from './signInResult'\nimport type { HostAuth } from './hostAuth'\nimport type { HostPrincipal } from './principal'\n\nenum GateStatus {\n Checking = 'checking',\n Authenticated = 'authenticated',\n Unauthenticated = 'unauthenticated',\n}\n\nexport interface AuthGateProps {\n children: ReactNode\n /** Override the default sign-in screen (branding). Defaults to `DefaultSignInScreen`. */\n signInScreen?: ReactNode\n /** Override the default full-screen loader. Defaults to a bare \"Fetching your data…\" spinner-less div. */\n loader?: ReactNode\n}\n\nexport interface AuthGateBundle {\n AuthGate: (props: AuthGateProps) => ReactNode\n DefaultSignInScreen: () => ReactNode\n}\n\n/**\n * MUST be called synchronously, at the top of the app's own auth-setup\n * module (as part of `createVibeAuth`) — same timing constraint as\n * `createCallbackHandler`: `RETURNING_FROM_OAUTH`/`IS_CALLBACK_PATH` must be\n * captured before any effect runs, and before supabase-js's\n * `detectSessionInUrl` can strip the URL.\n */\nexport function createAuthGate(\n config: ResolvedVibeAuthConfig,\n silentAuth: SilentAuth,\n interactiveAuth: InteractiveAuth,\n hostAuth?: HostAuth,\n): AuthGateBundle {\n const { supabase, callbackPath, timeouts } = config\n\n // Did a top-level interactive sign-in just redirect back here with an\n // OAuth result in the URL? Captured at FACTORY-CALL SCOPE for the same\n // reason as the callback handler: supabase-js's `detectSessionInUrl`\n // strips the code/hash asynchronously once the client boots, so reading\n // `window.location` inside an effect races it.\n // The `access_token` check is NOT support for implicit flow (PKCE is\n // mandatory) — it's a regression detector: if the client ever loses\n // `flowType: 'pkce'`, the provider returns tokens in the hash, and without\n // this check the gate would fire a silent attempt on that mount and\n // clobber the in-progress return.\n const RETURNING_FROM_OAUTH =\n typeof window !== 'undefined' &&\n (new URLSearchParams(window.location.search).has('code') ||\n window.location.hash.includes('access_token'))\n\n // Same idea: capture early, but don't let it affect the FIRST render's\n // JSX — see the isCallback/useState comment below for why.\n const IS_CALLBACK_PATH =\n typeof window !== 'undefined' && window.location.pathname === callbackPath\n\n function DefaultFullScreenLoader() {\n return (\n <div\n style={{ display: 'grid', placeItems: 'center', height: '100vh', fontFamily: 'sans-serif' }}\n >\n Fetching your data…\n </div>\n )\n }\n\n function DefaultSignInScreen() {\n const [pending, setPending] = useState(false)\n const [failure, setFailure] = useState<SignInFailureReason | null>(null)\n return (\n <div\n style={{ display: 'grid', placeItems: 'center', height: '100vh', fontFamily: 'sans-serif' }}\n >\n <div style={{ textAlign: 'center' }}>\n <button\n type=\"button\"\n disabled={pending}\n onClick={() => {\n setPending(true)\n setFailure(null)\n void interactiveAuth.signInInteractive().then((result) => {\n setFailure(result.kind === SignInKind.Failed ? result.reason : null)\n // Redirecting keeps `pending` — this page is unloading, and\n // re-enabling the button would invite a second, doomed click.\n if (result.kind !== SignInKind.Redirecting) setPending(false)\n })\n }}\n >\n {pending ? 'Opening sign-in…' : 'Sign in'}\n </button>\n {failure ? (\n <p style={{ fontSize: 12, marginTop: 8 }}>\n {failure === SignInFailureReason.PopupBlocked\n ? 'Your browser blocked the sign-in popup. Allow popups for this site and try again.'\n : \"Sign-in didn't complete. Please try again.\"}\n </p>\n ) : null}\n </div>\n </div>\n )\n }\n\n // Wrap the ENTIRE app: <AuthGate><App /></AuthGate>. The app is `children`,\n // so it never mounts until `authenticated`.\n function AuthGate({ children, signInScreen, loader }: AuthGateProps): ReactNode {\n // /silent-callback loads INSIDE the hidden iframe (silent) and inside\n // the sign-in popup (interactive), and must render UNGATED in both. If\n // the gate intercepts it, it can't postMessage its result back and the\n // flow hangs forever. (Cleanest is to mount the callback route OUTSIDE\n // the gate; this in-gate exemption is the safety net for when the gate\n // wraps the whole app.)\n //\n // Neither `isCallback` nor `status` may depend on `window` at their\n // INITIAL value: in an SSR app, the server always renders with no\n // `window`, so if the client's first hydration pass evaluated real\n // `window.location` here instead, server and client would render\n // different things on the very first paint — \"Hydration failed\"\n // pointing at this component. Both start at their server-safe default\n // and get corrected in an effect, one tick after mount (a harmless\n // second render, not a hydration mismatch).\n const [isCallback, setIsCallback] = useState(false)\n const [status, setStatus] = useState<GateStatus>(GateStatus.Checking)\n\n useEffect(() => {\n if (IS_CALLBACK_PATH) setIsCallback(true)\n }, [])\n\n useEffect(() => {\n // Guard on the FACTORY-SCOPE CONSTANT, never the `isCallback` state:\n // the state is corrected one tick AFTER mount, so on the first commit\n // — even on the callback route itself — it is still `false`. Gating on\n // it would run resolve() inside the callback page and kick off a\n // redundant sign-in attempt there, interfering with the exchange it is\n // supposed to be completing. Only the RENDERED OUTPUT needs the\n // deferred state (for hydration); the logic wants the\n // immediately-correct value.\n if (IS_CALLBACK_PATH) return // never gate the callback route\n let cancelled = false\n let oauthTimer: number | undefined\n // Set when we learn the host principal. `ensureSession` must not open\n // the gate after this is set — the leftover session is unverified.\n let hostPrincipal: HostPrincipal | undefined\n // Bumps on every principal apply so an older async completion cannot\n // reopen the gate after a newer rebind/logout/host transition.\n let applyGen = 0\n // Host-wired boot starts this read alongside the seed wait. The first\n // principal apply shares it instead of paying for getSession() afterward.\n let pendingSessionRead: { promise: Promise<boolean>; settled: boolean } | undefined\n // Live AUTH_CHANGED / late seed share applyPrincipal. Host-wired starts\n // false until resolve() is about to wait, then true so a burst seed\n // isn't dropped after the waiter unsubscribes.\n let acceptPrincipalUpdates = !hostAuth?.isHostWired()\n // True once THIS document has observed a SIGNED_IN while its URL carried\n // the OAuth result — i.e. detectSessionInUrl redeemed our own code here.\n // That is the provenance `completeWithResult` verifies before stamping,\n // and the only thing that makes stamping below safe: a session merely\n // sitting in storage during a failed exchange must never be stamped.\n let redeemedHere = false\n\n /**\n * Is the live session provably this principal's?\n *\n * Both `applyPrincipal` and `openIfBound` need this answer, and used to\n * compute it separately. `sessionOk` is a parameter because\n * `applyPrincipal` has a prefetched read to reuse.\n *\n * A session this document just minted from its own OAuth return is bound\n * by construction — the redirect flow never reaches `completeWithResult`,\n * so the stamp happens here, in one place.\n */\n const sessionBelongsTo = (principal: HostPrincipal, sessionOk: boolean): boolean => {\n if (!sessionOk) return false\n if (silentAuth.isBoundTo(principal)) return true\n if (!redeemedHere) return false\n silentAuth.stampBoundMarker(principal)\n return true\n }\n\n const applyPrincipal = async (principal: HostPrincipal): Promise<void> => {\n hostPrincipal = principal\n const gen = ++applyGen\n // Hide any prior tenant/user content before the async rebind lands.\n setStatus(GateStatus.Checking)\n const prefetchedSession = pendingSessionRead\n pendingSessionRead = undefined\n const inFlightPrefetch =\n prefetchedSession && !prefetchedSession.settled ? prefetchedSession : undefined\n let sessionOk = inFlightPrefetch\n ? await inFlightPrefetch.promise\n : await silentAuth.hasValidSession().catch(() => false)\n // Any read started before the host principal arrived can settle with a\n // stale true or false. Confirm its result once against current storage.\n if (inFlightPrefetch) {\n sessionOk = await silentAuth.hasValidSession().catch(() => false)\n }\n if (cancelled || gen !== applyGen) return\n if (sessionBelongsTo(principal, sessionOk)) {\n setStatus(GateStatus.Authenticated)\n return\n }\n // Mid-return, the code in the URL has not been redeemed yet (or its\n // SIGNED_IN has not landed). Do NOT rebind: rebindSession is silent-only\n // and every signInWithOAuth mints a fresh code_verifier, invalidating\n // the very code being redeemed. Wait for SIGNED_IN; oauthTimer is the\n // backstop if it never comes.\n if (RETURNING_FROM_OAUTH) return\n const ok = await silentAuth.rebindSession(principal).catch(() => false)\n if (cancelled || gen !== applyGen) return\n if (!ok) void supabase.auth.signOut().catch(() => {})\n setStatus(ok ? GateStatus.Authenticated : GateStatus.Unauthenticated)\n }\n\n /** Open only when the live session is still bound to the host principal. */\n const openIfBound = async (): Promise<void> => {\n if (cancelled) return\n if (hostAuth?.isHostWired() && !hostPrincipal) return\n const gen = applyGen\n const principal = hostPrincipal\n if (principal) {\n const sessionOk = await silentAuth.hasValidSession().catch(() => false)\n if (cancelled || gen !== applyGen) return\n if (!sessionBelongsTo(principal, sessionOk)) return\n }\n setStatus(GateStatus.Authenticated)\n }\n\n async function resolve(): Promise<void> {\n // A top-level interactive sign-in just landed back here with its\n // result in the URL. Do NOT start a silent attempt now: every\n // signInWithOAuth() call mints a FRESH PKCE code_verifier and\n // overwrites the stored one, which would invalidate the very code\n // being redeemed right now (`bad_code_verifier`, 400 on\n // /token?grant_type=pkce). Let supabase-js's `detectSessionInUrl`\n // finish the exchange; its SIGNED_IN event opens the gate via the\n // listener below. The timeout is the fallback for an exchange that\n // fails or never fires.\n //\n // This branch is checked FIRST, before any silent-signin attempt,\n // and must survive StrictMode's mount→cleanup→remount: each mount\n // reschedules its own timer, and cleanup clears only its own — so\n // the surviving (second) mount always ends up with a live timer.\n // Rescheduling a fresh timer per mount is harmless.\n if (RETURNING_FROM_OAUTH) {\n // resolve() returns here without reaching the host-wired branch that\n // normally opens this gate, so a seed arriving now would be dropped\n // and hostPrincipal would stay undefined — leaving openIfBound unable\n // to ever open, and the timer below the only outcome.\n acceptPrincipalUpdates = true\n oauthTimer = window.setTimeout(() => {\n if (!cancelled)\n setStatus((s) => (s === GateStatus.Checking ? GateStatus.Unauthenticated : s))\n }, timeouts.oauthCompletionMs)\n return\n }\n\n // Host-wired: wait for seed, then open if the marker already matches\n // this `{userId, tenant}` — otherwise rebind behind the loader.\n // Seed from a parent of AuthGate; seeding inside children deadlocks.\n // Accept live updates BEFORE the wait so a burst seed after the\n // waiter unsubscribes still reaches applyPrincipal. If the\n // subscription already applied, skip the waiter's (possibly stale) value.\n // Always return: a leftover session is not a host identity.\n if (hostAuth?.isHostWired()) {\n acceptPrincipalUpdates = true\n const sessionRead = {\n promise: silentAuth.hasValidSession().catch(() => false),\n settled: false,\n }\n void sessionRead.promise.finally(() => {\n sessionRead.settled = true\n })\n pendingSessionRead = sessionRead\n const next = await hostAuth.waitForSeededPrincipal(timeouts.oauthCompletionMs)\n if (cancelled) return\n if (next && applyGen === 0) await applyPrincipal(next)\n if (pendingSessionRead === sessionRead) pendingSessionRead = undefined\n if (applyGen === 0) setStatus(GateStatus.Unauthenticated)\n return\n }\n\n if (await silentAuth.ensureSession().catch(() => false)) {\n if (!cancelled && !hostPrincipal) setStatus(GateStatus.Authenticated)\n return\n }\n\n // No signOut() on failure — a sibling may still hold a good session.\n if (!cancelled && !hostPrincipal) setStatus(GateStatus.Unauthenticated)\n }\n\n void resolve()\n\n // Live host principal changes (and late seed when wireHostAuth ran after\n // mount) share applyPrincipal with the initial seed path above.\n const unsubPrincipal = hostAuth?.onSeededPrincipal((next) => {\n if (cancelled || !acceptPrincipalUpdates) return\n void applyPrincipal(next)\n })\n\n // A sibling document or another tab can establish a session AFTER\n // we've gated. `storage` fires in OTHER documents of this origin when\n // localStorage changes, and is the only cross-document signal\n // available — supabase's onAuthStateChange does not cross documents.\n // Without this, a late winner leaves this copy showing the sign-in\n // screen until reload. Removals stay closed; SIGNED_OUT closes an open gate.\n const onStorage = (e: StorageEvent) => {\n if (cancelled) return\n if (!e.key?.endsWith('-auth-token')) return\n pendingSessionRead = undefined\n if (!e.newValue) return\n void openIfBound()\n }\n window.addEventListener('storage', onStorage)\n\n // Re-close the gate on sign-out; open it once a manual sign-in\n // completes. IGNORE `INITIAL_SESSION`: it fires on mount carrying any\n // *persisted* Supabase session, and acting on it would open the gate\n // before `resolve()` re-validates live SSO — the exact stale-session\n // trust this package forbids. `resolve()` is the sole authority for\n // the initial open.\n //\n // v1 FIX (see file header): a null session may only CLOSE an OPEN\n // gate, never downgrade `checking`. `resolve()` above is the sole\n // authority for the FIRST verdict — a stale session's boot-time\n // refresh failing (SIGNED_OUT) is not a reason to preempt it.\n const { data: sub } = supabase.auth.onAuthStateChange((event, session) => {\n if (cancelled || event === 'INITIAL_SESSION') return\n if (session) {\n // Provenance for sessionBelongsTo: a session created in this document\n // while its URL carried the OAuth result is ours to stamp.\n if (RETURNING_FROM_OAUTH && event === 'SIGNED_IN') redeemedHere = true\n void openIfBound()\n } else {\n pendingSessionRead = undefined\n setStatus((s) => (s === GateStatus.Authenticated ? GateStatus.Unauthenticated : s))\n }\n })\n\n return () => {\n cancelled = true\n unsubPrincipal?.()\n if (oauthTimer !== undefined) window.clearTimeout(oauthTimer)\n sub.subscription.unsubscribe()\n window.removeEventListener('storage', onStorage)\n }\n }, []) // IS_CALLBACK_PATH is a stable constant, so this runs once\n\n if (isCallback) return <>{children}</> // callback route renders ungated\n if (status === GateStatus.Checking) return loader ?? <DefaultFullScreenLoader />\n if (status === GateStatus.Unauthenticated) return signInScreen ?? <DefaultSignInScreen /> // the ONLY login UI\n return <>{children}</>\n }\n\n return { AuthGate, DefaultSignInScreen }\n}\n","// Host logout & identity-switch handling, ported from the pre-package\n// silent-supabase-oidc-login skill code.\nimport type { ResolvedVibeAuthConfig } from './config'\nimport type { HostPrincipal } from './principal'\nimport type { SilentAuth } from './silentAuth'\n\n/** The subset of `VibeAppBridge` this module needs — structural, not a hard dependency. */\nexport interface AuthBridgeLike {\n onAuthChange(\n cb: (auth: { authenticated: boolean; userId?: string; tenant?: string }) => void,\n ): () => void\n}\n\nexport interface HostAuth {\n isHostWired(): boolean\n waitForSeededPrincipal(timeoutMs: number): Promise<HostPrincipal | null>\n onSeededPrincipal(cb: (principal: HostPrincipal) => void): () => void\n /**\n * Wire BEFORE `bridge.connect()`, at module scope so `AuthGate` sees\n * `isHostWired()` on first resolve.\n */\n wireHostAuth(bridge: AuthBridgeLike): () => void\n /**\n * Seed `{userId, tenant}` from `connect()`, from a **parent** of `AuthGate`.\n */\n seedLastUserId(userId: string, tenant: string): void\n /** Current seeded host `{userId, tenant}`, if any. */\n getPrincipal(): HostPrincipal | undefined\n}\n\nexport function createHostAuth(\n config: ResolvedVibeAuthConfig,\n silentAuth: Pick<SilentAuth, 'clearBoundUser'>,\n): HostAuth {\n const { supabase } = config\n\n let principal: HostPrincipal | undefined\n let hostWired = false\n const seedListeners = new Set<(next: HostPrincipal) => void>()\n\n function isHostWired(): boolean {\n return hostWired\n }\n\n function onSeededPrincipal(cb: (next: HostPrincipal) => void): () => void {\n seedListeners.add(cb)\n return () => {\n seedListeners.delete(cb)\n }\n }\n\n function waitForSeededPrincipal(timeoutMs: number): Promise<HostPrincipal | null> {\n if (principal) return Promise.resolve(principal)\n return new Promise((resolve) => {\n let settled = false\n const finish = (value: HostPrincipal | null) => {\n if (settled) return\n settled = true\n window.clearTimeout(timer)\n off()\n resolve(value)\n }\n const timer = window.setTimeout(() => finish(null), timeoutMs)\n const off = onSeededPrincipal((next) => finish(next))\n if (principal) finish(principal)\n })\n }\n\n function seedLastUserId(userId: string, tenant: string): void {\n principal = { userId, tenant }\n for (const listener of [...seedListeners]) listener(principal)\n }\n\n function wireHostAuth(bridge: AuthBridgeLike): () => void {\n hostWired = true\n return bridge.onAuthChange((auth) => {\n if (!auth.authenticated) {\n principal = undefined\n silentAuth.clearBoundUser()\n void supabase.auth.signOut().catch(() => {})\n return\n }\n if (!auth.userId) return\n const tenant = auth.tenant ?? principal?.tenant\n if (!tenant) {\n console.warn('[vibe-auth] AUTH_CHANGED authenticated without tenant; ignoring')\n return\n }\n if (principal?.userId === auth.userId && principal.tenant === tenant) return\n // Same path as seedLastUserId: AuthGate.applyPrincipal owns rebind/UI.\n // Notifying here (not rebinding inline) hides stale content behind the loader.\n seedLastUserId(auth.userId, tenant)\n })\n }\n\n return {\n wireHostAuth,\n seedLastUserId,\n getPrincipal: () => principal,\n isHostWired,\n waitForSeededPrincipal,\n onSeededPrincipal,\n }\n}\n","// The whole host handshake as one call — the packaged replacement for the\n// four-step module every Vibe App used to hand-write:\n//\n// new VibeAppBridge() → onContextChange → wireHostAuth → connect().then(seedLastUserId)\n//\n// Four of seven migrated apps got that sequence wrong, each differently: one\n// put `wireHostAuth` in a lazy() chunk and `connect()` in a useEffect; one\n// registered `onContextChange` after `connect()` had already started; one\n// mounted the bridge provider inside AuthGate (deadlock); one split it across a\n// helper with its own competing `onAuthChange`. None of those are reachable\n// through this function.\nimport type { ResolvedVibeAuthConfig } from './config'\nimport type { AuthBridgeLike, HostAuth } from './hostAuth'\nimport type {\n HostContextLike,\n VibeAppWiring,\n VibeBridgeLike,\n WireVibeAppOptions,\n} from './vibeAppWiring'\n\nexport function createWireVibeApp(config: ResolvedVibeAuthConfig, hostAuth: HostAuth) {\n /**\n * Wire a `VibeAppBridge` to this auth instance and start the host handshake\n * — every step, in the one correct order.\n *\n * Call it **once**, synchronously, at module scope of an **eagerly imported**\n * module (conventionally the same `src/lib/vibe-bridge.ts` that exports the\n * bridge singleton). Not from a React effect, and not from a module reachable\n * only through a `lazy()` chunk: `AuthGate` holds its loader until the host\n * principal is seeded, so deferring this is visible to users as a slow app.\n *\n * The bridge is passed in rather than constructed here — this package has no\n * dependency on `@nominalso/vibe-bridge`, and adopting an instance also lets\n * an app keep its own memoised factory or request wrapper.\n *\n * @example\n * ```ts\n * // src/lib/vibe-bridge.ts\n * import { VibeAppBridge } from '@nominalso/vibe-bridge'\n * import { auth } from './auth'\n *\n * export const bridge = new VibeAppBridge()\n *\n * export const { hostContext, getHostContext, subscribeHostContext } =\n * auth.wireVibeApp(bridge, {\n * onContextChange: () => queryClient.invalidateQueries(),\n * })\n * ```\n */\n return function wireVibeApp<Ctx extends HostContextLike>(\n bridge: VibeBridgeLike,\n options: WireVibeAppOptions<Ctx> = {},\n ): VibeAppWiring<Ctx> {\n const listeners = new Set<(ctx: Ctx | null) => void>()\n let current: Ctx | null = null\n\n function publish(next: Ctx | null): void {\n current = next\n for (const listener of [...listeners]) listener(next)\n }\n\n // 1. Context subscriber BEFORE connect(). The bridge keeps no backlog, so a\n // push that lands before this is registered is lost outright.\n bridge.onContextChange((next) => {\n publish(next as Ctx)\n options.onContextChange?.(next as Ctx)\n })\n\n // Same ordering rule for the other single-subscriber listeners.\n // `onDataReset` is deliberately deferred — see below.\n if (options.onSubrouteRequest) bridge.onSubrouteRequest?.(options.onSubrouteRequest)\n\n // 2. wireHostAuth BEFORE connect(), so AuthGate sees a host is wired on its\n // first resolve and waits for the principal instead of opening on\n // whatever leftover Supabase session happens to be in storage.\n // `onAuthChange` is @internal in the bridge and stripped from its published\n // types, so it is absent from VibeBridgeLike. It exists at runtime, and this\n // is the one sanctioned caller — hence the local cast.\n hostAuth.wireHostAuth(bridge as unknown as AuthBridgeLike)\n\n // 3. Skip the handshake where there is no host to reach. The OAuth callback\n // document (a hidden iframe during silent SSO, a popup during\n // interactive sign-in) imports this module like any other but has no\n // Nominal host: connecting there burns a full handshake timeout and can\n // disturb the relay. A module-scope decision — never branch *rendered\n // output* on `window`, which is the React #418 hydration bug.\n const noHost = typeof window === 'undefined' || window.location.pathname === config.callbackPath\n\n const hostContext: Promise<Ctx | null> = noHost\n ? Promise.resolve(null)\n : bridge\n .connect()\n .then((ctx) => {\n const next = ctx as Ctx\n // 4. Seed the principal AuthGate is waiting on BEFORE publishing, so\n // a context subscriber can never observe a context the gate has\n // not yet been told about.\n hostAuth.seedLastUserId(next.user.id, next.tenant)\n // Only now do we know whether the host enabled data reset.\n if (next.enableDataReset && options.onDataReset) {\n bridge.onDataReset?.(options.onDataReset)\n }\n publish(next)\n return next\n })\n .catch((error: unknown) => {\n // Expected outside the Nominal host. Do NOT rethrow, and do NOT sign\n // out: a failed handshake is not evidence the user is logged out.\n console.warn('[vibe-auth] host connect failed', error)\n return null\n })\n\n return {\n bridge,\n hostContext,\n getHostContext: () => current,\n subscribeHostContext: (listener) => {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n },\n }\n }\n}\n","import { resolveConfig, assertPkce, type VibeAuthConfig } from './config'\nimport { createSilentAuth } from './silentAuth'\nimport { createInteractiveAuth } from './interactive'\nimport { createCallbackHandler } from './SilentCallback'\nimport { createAuthGate } from './AuthGate'\nimport { createHostAuth, type AuthBridgeLike, type HostAuth } from './hostAuth'\nimport { createWireVibeApp } from './wireVibeApp'\nimport type {\n HostContextLike,\n VibeAppWiring,\n VibeBridgeLike,\n WireVibeAppOptions,\n} from './vibeAppWiring'\nimport type { HostPrincipal } from './principal'\nimport type { SignInResult } from './signInResult'\nimport { RESULT_MESSAGE_TYPE } from './authMessage'\n\nexport interface VibeAuth {\n // core\n /** True when a Supabase session already exists locally. */\n hasValidSession(): Promise<boolean>\n /**\n * Fresh sign-in as a NEW host identity, coordinated across documents. Call\n * this from your host-auth handler on an identity switch — NOT `ensureSession`.\n * Prefer `wireHostAuth`, which already calls this correctly.\n */\n rebindSession(principal: HostPrincipal): Promise<boolean>\n /**\n * Runs the `prompt=none` silent flow once, deduped within this document.\n * Most apps want `ensureSession` (the gate calls it); this is exposed for\n * advanced cases like a post-handshake retry.\n */\n trySilentSignIn(): Promise<boolean>\n /** Gets a session, coordinated across every document and tab on this origin. */\n ensureSession(): Promise<boolean>\n /**\n * The interactive (popup / top-level redirect) fallback. `AuthGate`'s\n * default sign-in screen calls this. Note `redirecting` is not a failure:\n * the page is navigating to the IdP and is about to unload.\n */\n signInInteractive(): Promise<SignInResult>\n /** Remove the identity-switch bound-user marker. Call on host logout, next to signOut(). */\n clearBoundUser(): void\n\n // react\n /** Wrap the ENTIRE app: `<AuthGate><App /></AuthGate>`. The app never mounts until authenticated. */\n AuthGate: ReturnType<typeof createAuthGate>['AuthGate']\n /** The default sign-in screen `AuthGate` renders unless you pass `signInScreen`. */\n DefaultSignInScreen: ReturnType<typeof createAuthGate>['DefaultSignInScreen']\n /** Mount this, and ONLY this, at `callbackPath` — ungated, outside `AuthGate`. */\n SilentCallback: ReturnType<typeof createCallbackHandler>['SilentCallback']\n\n // bridge integration\n /**\n * **The one call an embedded app needs.** Wires the bridge to this auth\n * instance and starts the host handshake — `onContextChange`, `wireHostAuth`,\n * the callback-document guard, `connect()`, and `seedLastUserId` — in the\n * one correct order. Returns a context store plus the connect promise.\n *\n * Call it once, synchronously, at module scope of an eagerly imported module.\n * Prefer this over calling `wireHostAuth` / `seedLastUserId` by hand.\n */\n wireVibeApp<Ctx extends HostContextLike>(\n bridge: VibeBridgeLike,\n options?: WireVibeAppOptions<Ctx>,\n ): VibeAppWiring<Ctx>\n /**\n * Wire the host's auth signal to this Supabase client. Call at module\n * scope, BEFORE `bridge.connect()`, so `AuthGate` will not open on a\n * leftover Supabase session before it knows the current Nominal user.\n * Returns the unsubscribe function.\n *\n * Low-level: `wireVibeApp` already does this in the right order. Reach for\n * this only when an app genuinely cannot hand its bridge over.\n */\n wireHostAuth(bridge: AuthBridgeLike): () => void\n /**\n * Seed `{userId, tenant}` from `connect()`. Call from a **parent** of\n * `AuthGate` after `connect()` resolves. Start `connect()` at module scope,\n * not in a React effect, so the gate receives the live host principal as\n * early as possible. It then opens without rebind when the bound-user\n * marker already matches.\n */\n seedLastUserId(userId: string, tenant: string): void\n\n // constants\n /** The postMessage type relayed by `SilentCallback` — do not change if migrating an existing app. */\n RESULT_MESSAGE_TYPE: string\n /** The resolved callback path (default `/silent-callback`). */\n callbackPath: string\n}\n\n/**\n * Creates a configured, self-contained silent-SSO auth instance for a Nominal\n * Vibe App — the packaged, fixed replacement for a hand-copied\n * `silentAuth.ts`/`AuthGate.tsx` generated from the silent-supabase-oidc-login\n * skill.\n *\n * MUST be called synchronously, at the top of the app's own auth-setup\n * module (e.g. `src/lib/auth.ts`), and that module must be imported eagerly\n * (never lazily) — `AuthGate`/`SilentCallback` capture the callback URL at\n * this call, before any React effect or async work can let supabase-js's\n * `detectSessionInUrl` strip it first.\n *\n * @example\n * ```ts\n * // src/lib/auth.ts\n * import { createVibeAuth } from '@nominalso/vibe-auth'\n * import { supabase } from './supabaseClient'\n *\n * export const auth = createVibeAuth({\n * supabase,\n * provider: 'custom:supabase-fedapp', // from your Supabase Auth → Providers config\n * })\n * ```\n *\n * ```tsx\n * // src/routes/silent-callback.tsx (TanStack Start; ssr: false on this route)\n * import { auth } from '@/lib/auth'\n * export default auth.SilentCallback\n * ```\n *\n * ```tsx\n * // wrap the app root — see AGENTS.md for the SSR-safe recipe\n * <auth.AuthGate>\n * <App />\n * </auth.AuthGate>\n * ```\n *\n * ```ts\n * // Start the host handshake at module scope, before React mounts.\n * const unsub = auth.wireHostAuth(bridge)\n * export const hostContext = bridge.connect().then((ctx) => {\n * auth.seedLastUserId(ctx.user.id, ctx.tenant)\n * return ctx\n * })\n * ```\n */\nexport function createVibeAuth(userConfig: VibeAuthConfig): VibeAuth {\n const config = resolveConfig(userConfig)\n assertPkce(config.supabase)\n\n let hostAuth: HostAuth\n const silentAuth = createSilentAuth(\n config,\n () => hostAuth.getPrincipal(),\n (principal) => hostAuth.seedLastUserId(principal.userId, principal.tenant),\n )\n const interactiveAuth = createInteractiveAuth(config, silentAuth)\n const { SilentCallback } = createCallbackHandler()\n hostAuth = createHostAuth(config, silentAuth)\n const { AuthGate, DefaultSignInScreen } = createAuthGate(\n config,\n silentAuth,\n interactiveAuth,\n hostAuth,\n )\n const { wireHostAuth, seedLastUserId } = hostAuth\n\n return {\n hasValidSession: silentAuth.hasValidSession,\n rebindSession: silentAuth.rebindSession,\n trySilentSignIn: silentAuth.trySilentSignIn,\n ensureSession: silentAuth.ensureSession,\n signInInteractive: interactiveAuth.signInInteractive,\n clearBoundUser: silentAuth.clearBoundUser,\n AuthGate,\n DefaultSignInScreen,\n SilentCallback,\n wireVibeApp: createWireVibeApp(config, hostAuth),\n wireHostAuth,\n seedLastUserId,\n RESULT_MESSAGE_TYPE,\n callbackPath: config.callbackPath,\n }\n}\n"],"mappings":"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","names":["AuthResultKind","SignInKind","SignInFailureReason","import_react"]}