@nominalso/vibe-auth 0.2.0 → 0.2.2

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.d.cts CHANGED
@@ -222,7 +222,7 @@ interface AuthGateProps {
222
222
  children: ReactNode;
223
223
  /** Override the default sign-in screen (branding). Defaults to `DefaultSignInScreen`. */
224
224
  signInScreen?: ReactNode;
225
- /** Override the default full-screen loader. Defaults to a bare "Signing you in…" spinner-less div. */
225
+ /** Override the default full-screen loader. Defaults to a bare "Fetching your data…" spinner-less div. */
226
226
  loader?: ReactNode;
227
227
  }
228
228
  interface AuthGateBundle {
@@ -278,8 +278,10 @@ interface VibeAuth {
278
278
  wireHostAuth(bridge: AuthBridgeLike): () => void;
279
279
  /**
280
280
  * Seed `{userId, tenant}` from `connect()`. Call from a **parent** of
281
- * `AuthGate` after `connect()` resolves. The gate waits for this, then
282
- * opens without rebind when the bound-user marker already matches.
281
+ * `AuthGate` after `connect()` resolves. Start `connect()` at module scope,
282
+ * not in a React effect, so the gate receives the live host principal as
283
+ * early as possible. It then opens without rebind when the bound-user
284
+ * marker already matches.
283
285
  */
284
286
  seedLastUserId(userId: string, tenant: string): void;
285
287
  /** The postMessage type relayed by `SilentCallback` — do not change if migrating an existing app. */
@@ -325,10 +327,12 @@ interface VibeAuth {
325
327
  * ```
326
328
  *
327
329
  * ```ts
328
- * // wire the host bridge before connect()
330
+ * // Start the host handshake at module scope, before React mounts.
329
331
  * const unsub = auth.wireHostAuth(bridge)
330
- * const ctx = await bridge.connect()
331
- * auth.seedLastUserId(ctx.user.id, ctx.tenant)
332
+ * export const hostContext = bridge.connect().then((ctx) => {
333
+ * auth.seedLastUserId(ctx.user.id, ctx.tenant)
334
+ * return ctx
335
+ * })
332
336
  * ```
333
337
  */
334
338
  declare function createVibeAuth(userConfig: VibeAuthConfig): VibeAuth;
package/dist/index.d.ts CHANGED
@@ -222,7 +222,7 @@ interface AuthGateProps {
222
222
  children: ReactNode;
223
223
  /** Override the default sign-in screen (branding). Defaults to `DefaultSignInScreen`. */
224
224
  signInScreen?: ReactNode;
225
- /** Override the default full-screen loader. Defaults to a bare "Signing you in…" spinner-less div. */
225
+ /** Override the default full-screen loader. Defaults to a bare "Fetching your data…" spinner-less div. */
226
226
  loader?: ReactNode;
227
227
  }
228
228
  interface AuthGateBundle {
@@ -278,8 +278,10 @@ interface VibeAuth {
278
278
  wireHostAuth(bridge: AuthBridgeLike): () => void;
279
279
  /**
280
280
  * Seed `{userId, tenant}` from `connect()`. Call from a **parent** of
281
- * `AuthGate` after `connect()` resolves. The gate waits for this, then
282
- * opens without rebind when the bound-user marker already matches.
281
+ * `AuthGate` after `connect()` resolves. Start `connect()` at module scope,
282
+ * not in a React effect, so the gate receives the live host principal as
283
+ * early as possible. It then opens without rebind when the bound-user
284
+ * marker already matches.
283
285
  */
284
286
  seedLastUserId(userId: string, tenant: string): void;
285
287
  /** The postMessage type relayed by `SilentCallback` — do not change if migrating an existing app. */
@@ -325,10 +327,12 @@ interface VibeAuth {
325
327
  * ```
326
328
  *
327
329
  * ```ts
328
- * // wire the host bridge before connect()
330
+ * // Start the host handshake at module scope, before React mounts.
329
331
  * const unsub = auth.wireHostAuth(bridge)
330
- * const ctx = await bridge.connect()
331
- * auth.seedLastUserId(ctx.user.id, ctx.tenant)
332
+ * export const hostContext = bridge.connect().then((ctx) => {
333
+ * auth.seedLastUserId(ctx.user.id, ctx.tenant)
334
+ * return ctx
335
+ * })
332
336
  * ```
333
337
  */
334
338
  declare function createVibeAuth(userConfig: VibeAuthConfig): VibeAuth;
package/dist/index.js CHANGED
@@ -47,6 +47,7 @@ function assertPkce(supabase) {
47
47
  var RESULT_MESSAGE_TYPE = "silent-auth-result";
48
48
 
49
49
  // src/silentAuth.ts
50
+ var SIBLING_SESSION_SYNC_MS = 50;
50
51
  var AuthResultKind = /* @__PURE__ */ ((AuthResultKind2) => {
51
52
  AuthResultKind2["Code"] = "code";
52
53
  return AuthResultKind2;
@@ -59,7 +60,7 @@ function parseCallbackMessage(data) {
59
60
  }
60
61
  return null;
61
62
  }
62
- function createSilentAuth(config, getHostPrincipal) {
63
+ function createSilentAuth(config, getHostPrincipal, onBound) {
63
64
  const { supabase, provider, callbackPath, timeouts, lockName, boundUserKey } = config;
64
65
  async function getSessionUserId() {
65
66
  const { data } = await supabase.auth.getSession();
@@ -68,6 +69,11 @@ function createSilentAuth(config, getHostPrincipal) {
68
69
  async function hasValidSession() {
69
70
  return await getSessionUserId() !== null;
70
71
  }
72
+ async function waitForSiblingSession() {
73
+ if (await hasValidSession()) return true;
74
+ await new Promise((resolve) => setTimeout(resolve, SIBLING_SESSION_SYNC_MS));
75
+ return hasValidSession();
76
+ }
71
77
  function readBoundMarker() {
72
78
  if (typeof window === "undefined") return null;
73
79
  try {
@@ -92,7 +98,10 @@ function createSilentAuth(config, getHostPrincipal) {
92
98
  if (priorUserId !== null && sessionUserId === priorUserId) return false;
93
99
  }
94
100
  const host = getHostPrincipal?.();
95
- if (host) writeBoundMarker(host);
101
+ if (host) {
102
+ writeBoundMarker(host);
103
+ onBound?.(host);
104
+ }
96
105
  return true;
97
106
  }
98
107
  function runHiddenAuthFrame(url) {
@@ -190,7 +199,7 @@ function createSilentAuth(config, getHostPrincipal) {
190
199
  if (await hasValidSession()) return true;
191
200
  return withSsoLock(
192
201
  async () => {
193
- if (await hasValidSession()) return true;
202
+ if (await waitForSiblingSession()) return true;
194
203
  if (await trySilentSignIn()) return true;
195
204
  return hasValidSession();
196
205
  },
@@ -202,7 +211,14 @@ function createSilentAuth(config, getHostPrincipal) {
202
211
  if (typeof window === "undefined") return false;
203
212
  const siblingRebound = async () => {
204
213
  const m = readBoundMarker();
205
- return m?.userId === principal.userId && m?.tenant === principal.tenant && Date.now() - (m.at ?? 0) < timeouts.lockWaitMs && await hasValidSession();
214
+ if (m?.userId !== principal.userId) return false;
215
+ if (m.tenant !== principal.tenant) return false;
216
+ if (Date.now() - (m.at ?? 0) >= timeouts.lockWaitMs) return false;
217
+ if (!await waitForSiblingSession()) return false;
218
+ const confirmed = readBoundMarker();
219
+ if (confirmed?.userId !== principal.userId) return false;
220
+ if (confirmed.tenant !== principal.tenant) return false;
221
+ return Date.now() - (confirmed.at ?? 0) < timeouts.lockWaitMs;
206
222
  };
207
223
  return withSsoLock(
208
224
  async () => {
@@ -460,7 +476,7 @@ function createAuthGate(config, silentAuth, interactiveAuth, hostAuth) {
460
476
  "div",
461
477
  {
462
478
  style: { display: "grid", placeItems: "center", height: "100vh", fontFamily: "sans-serif" },
463
- children: "Signing you in\u2026"
479
+ children: "Fetching your data\u2026"
464
480
  }
465
481
  );
466
482
  }
@@ -505,12 +521,19 @@ function createAuthGate(config, silentAuth, interactiveAuth, hostAuth) {
505
521
  let oauthTimer;
506
522
  let hostPrincipal;
507
523
  let applyGen = 0;
524
+ let pendingSessionRead;
508
525
  let acceptPrincipalUpdates = !hostAuth?.isHostWired();
509
526
  const applyPrincipal = async (principal) => {
510
527
  hostPrincipal = principal;
511
528
  const gen = ++applyGen;
512
529
  setStatus("checking" /* Checking */);
513
- const sessionOk = await silentAuth.hasValidSession().catch(() => false);
530
+ const prefetchedSession = pendingSessionRead;
531
+ pendingSessionRead = void 0;
532
+ const inFlightPrefetch = prefetchedSession && !prefetchedSession.settled ? prefetchedSession : void 0;
533
+ let sessionOk = inFlightPrefetch ? await inFlightPrefetch.promise : await silentAuth.hasValidSession().catch(() => false);
534
+ if (inFlightPrefetch) {
535
+ sessionOk = await silentAuth.hasValidSession().catch(() => false);
536
+ }
514
537
  if (cancelled || gen !== applyGen) return;
515
538
  if (sessionOk && silentAuth.isBoundTo(principal)) {
516
539
  setStatus("authenticated" /* Authenticated */);
@@ -544,9 +567,18 @@ function createAuthGate(config, silentAuth, interactiveAuth, hostAuth) {
544
567
  }
545
568
  if (hostAuth?.isHostWired()) {
546
569
  acceptPrincipalUpdates = true;
570
+ const sessionRead = {
571
+ promise: silentAuth.hasValidSession().catch(() => false),
572
+ settled: false
573
+ };
574
+ void sessionRead.promise.finally(() => {
575
+ sessionRead.settled = true;
576
+ });
577
+ pendingSessionRead = sessionRead;
547
578
  const next = await hostAuth.waitForSeededPrincipal(timeouts.oauthCompletionMs);
548
579
  if (cancelled) return;
549
580
  if (next && applyGen === 0) await applyPrincipal(next);
581
+ if (pendingSessionRead === sessionRead) pendingSessionRead = void 0;
550
582
  if (applyGen === 0) setStatus("unauthenticated" /* Unauthenticated */);
551
583
  return;
552
584
  }
@@ -563,14 +595,19 @@ function createAuthGate(config, silentAuth, interactiveAuth, hostAuth) {
563
595
  });
564
596
  const onStorage = (e) => {
565
597
  if (cancelled) return;
566
- if (!e.key?.endsWith("-auth-token") || !e.newValue) return;
598
+ if (!e.key?.endsWith("-auth-token")) return;
599
+ pendingSessionRead = void 0;
600
+ if (!e.newValue) return;
567
601
  void openIfBound();
568
602
  };
569
603
  window.addEventListener("storage", onStorage);
570
604
  const { data: sub } = supabase.auth.onAuthStateChange((event, session) => {
571
605
  if (cancelled || event === "INITIAL_SESSION") return;
572
606
  if (session) void openIfBound();
573
- else setStatus((s) => s === "authenticated" /* Authenticated */ ? "unauthenticated" /* Unauthenticated */ : s);
607
+ else {
608
+ pendingSessionRead = void 0;
609
+ setStatus((s) => s === "authenticated" /* Authenticated */ ? "unauthenticated" /* Unauthenticated */ : s);
610
+ }
574
611
  });
575
612
  return () => {
576
613
  cancelled = true;
@@ -658,7 +695,11 @@ function createVibeAuth(userConfig) {
658
695
  const config = resolveConfig(userConfig);
659
696
  assertPkce(config.supabase);
660
697
  let hostAuth;
661
- const silentAuth = createSilentAuth(config, () => hostAuth.getPrincipal());
698
+ const silentAuth = createSilentAuth(
699
+ config,
700
+ () => hostAuth.getPrincipal(),
701
+ (principal) => hostAuth.seedLastUserId(principal.userId, principal.tenant)
702
+ );
662
703
  const interactiveAuth = createInteractiveAuth(config, silentAuth);
663
704
  const { SilentCallback } = createCallbackHandler();
664
705
  hostAuth = createHostAuth(config, silentAuth);