@nominalso/vibe-auth 0.2.4 → 0.3.1

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/AGENTS.md CHANGED
@@ -45,7 +45,7 @@ export const auth = createVibeAuth({
45
45
 
46
46
  ```ts
47
47
  export const bridge = new VibeAppBridge()
48
- export const { hostContext, getHostContext, subscribeHostContext } =
48
+ export const { hostContext, hostOutcome, getHostContext, subscribeHostContext } =
49
49
  auth.wireVibeApp<ContextPayload>(bridge)
50
50
  ```
51
51
 
@@ -57,6 +57,11 @@ export const auth = createVibeAuth({
57
57
  same call; each is a single subscriber, and routing them through here is what
58
58
  guarantees exactly one of each.
59
59
 
60
+ `hostOutcome` (alongside `hostContext`) settles `{ kind: 'connected' }`,
61
+ `{ kind: 'standalone' }` or `{ kind: 'failed', error }`. `hostContext`
62
+ resolves `null` for the last two alike, so use `hostOutcome` when the app
63
+ needs an error state that does not fire on every standalone dev load.
64
+
60
65
  `wireHostAuth` / `seedLastUserId` are the pre-0.2.3 hand-wiring. They are
61
66
  still exported for compatibility, but new code should not call them — four of
62
67
  seven migrated apps got that sequence wrong, each differently, which is why
package/README.md CHANGED
@@ -67,7 +67,7 @@ import { auth } from '@/lib/auth'
67
67
 
68
68
  export const bridge = new VibeAppBridge()
69
69
 
70
- export const { hostContext, getHostContext, subscribeHostContext } =
70
+ export const { hostContext, hostOutcome, getHostContext, subscribeHostContext } =
71
71
  auth.wireVibeApp<ContextPayload>(bridge)
72
72
  ```
73
73
 
@@ -77,6 +77,19 @@ context is published, the handshake skipped in the callback document, and `null`
77
77
  rather than a rejection when there is no host. Pass `onContextChange`,
78
78
  `onDataReset` and `onSubrouteRequest` as options in the same call.
79
79
 
80
+ **Telling "not embedded" from "the host broke".** `hostContext` resolves `null`
81
+ for both, which is why apps either showed an error banner on every standalone
82
+ dev load or showed none at all. `hostOutcome` carries the distinction:
83
+
84
+ ```ts
85
+ const { hostContext, hostOutcome } = auth.wireVibeApp<ContextPayload>(bridge)
86
+
87
+ const outcome = await hostOutcome // never rejects, settles with hostContext
88
+ // { kind: 'connected' }
89
+ // { kind: 'standalone' } — nothing was ever going to answer; expected
90
+ // { kind: 'failed', error } — embedded, and the host stayed silent
91
+ ```
92
+
80
93
  See [`AGENTS.md`](./AGENTS.md) for SSR root wiring and the full ordering rules.
81
94
  Timeouts live on `VibeAuthTimeouts` in the package `.d.ts`.
82
95
 
package/dist/index.cjs CHANGED
@@ -501,28 +501,108 @@ function createAuthGate(config, silentAuth, interactiveAuth, hostAuth) {
501
501
  const { supabase, callbackPath, timeouts } = config;
502
502
  const RETURNING_FROM_OAUTH = typeof window !== "undefined" && (new URLSearchParams(window.location.search).has("code") || window.location.hash.includes("access_token"));
503
503
  const IS_CALLBACK_PATH = typeof window !== "undefined" && window.location.pathname === callbackPath;
504
+ const SPIN_CLASS = "nominal-vibe-auth-spin";
505
+ function Spinner({ size = 18 }) {
506
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
507
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: `
508
+ @keyframes ${SPIN_CLASS} { to { transform: rotate(360deg) } }
509
+ .${SPIN_CLASS} { animation: ${SPIN_CLASS} 700ms linear infinite; transform-origin: 50% 50% }
510
+ @media (prefers-reduced-motion: reduce) { .${SPIN_CLASS} { animation-duration: 2.4s } }
511
+ ` }),
512
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
513
+ "svg",
514
+ {
515
+ className: SPIN_CLASS,
516
+ width: size,
517
+ height: size,
518
+ viewBox: "0 0 24 24",
519
+ fill: "none",
520
+ "aria-hidden": "true",
521
+ children: [
522
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
523
+ "circle",
524
+ {
525
+ cx: "12",
526
+ cy: "12",
527
+ r: "9",
528
+ stroke: "currentColor",
529
+ strokeOpacity: "0.18",
530
+ strokeWidth: "3"
531
+ }
532
+ ),
533
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
534
+ "path",
535
+ {
536
+ d: "M21 12a9 9 0 0 0-9-9",
537
+ stroke: "currentColor",
538
+ strokeWidth: "3",
539
+ strokeLinecap: "round"
540
+ }
541
+ )
542
+ ]
543
+ }
544
+ )
545
+ ] });
546
+ }
547
+ const SCREEN = {
548
+ display: "grid",
549
+ placeItems: "center",
550
+ height: "100vh",
551
+ fontFamily: 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
552
+ color: "#3f3f46",
553
+ background: "#fafafa"
554
+ };
504
555
  function DefaultFullScreenLoader() {
505
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
556
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: SCREEN, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
506
557
  "div",
507
558
  {
508
- style: { display: "grid", placeItems: "center", height: "100vh", fontFamily: "sans-serif" },
509
- children: "Fetching your data\u2026"
559
+ style: { display: "flex", alignItems: "center", gap: 10, fontSize: 14 },
560
+ role: "status",
561
+ "aria-live": "polite",
562
+ children: [
563
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {}),
564
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "Fetching your data\u2026" })
565
+ ]
510
566
  }
511
- );
567
+ ) });
512
568
  }
513
569
  function DefaultSignInScreen() {
514
570
  const [pending, setPending] = (0, import_react2.useState)(false);
515
571
  const [failure, setFailure] = (0, import_react2.useState)(null);
516
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
572
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: SCREEN, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
517
573
  "div",
518
574
  {
519
- style: { display: "grid", placeItems: "center", height: "100vh", fontFamily: "sans-serif" },
520
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { textAlign: "center" }, children: [
521
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
575
+ style: {
576
+ textAlign: "center",
577
+ background: "#fff",
578
+ border: "1px solid #e4e4e7",
579
+ borderRadius: 12,
580
+ padding: "28px 32px",
581
+ boxShadow: "0 1px 2px rgba(0,0,0,0.04)",
582
+ maxWidth: 340
583
+ },
584
+ children: [
585
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
522
586
  "button",
523
587
  {
524
588
  type: "button",
525
589
  disabled: pending,
590
+ style: {
591
+ display: "inline-flex",
592
+ alignItems: "center",
593
+ justifyContent: "center",
594
+ gap: 8,
595
+ minWidth: 168,
596
+ padding: "10px 20px",
597
+ fontFamily: "inherit",
598
+ fontSize: 14,
599
+ fontWeight: 500,
600
+ color: "#fff",
601
+ background: pending ? "#71717a" : "#18181b",
602
+ border: "none",
603
+ borderRadius: 8,
604
+ cursor: pending ? "default" : "pointer"
605
+ },
526
606
  onClick: () => {
527
607
  setPending(true);
528
608
  setFailure(null);
@@ -531,13 +611,16 @@ function createAuthGate(config, silentAuth, interactiveAuth, hostAuth) {
531
611
  if (result.kind !== "redirecting" /* Redirecting */) setPending(false);
532
612
  });
533
613
  },
534
- children: pending ? "Opening sign-in\u2026" : "Sign in"
614
+ children: [
615
+ pending ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, { size: 15 }) : null,
616
+ pending ? "Opening sign-in\u2026" : "Sign in"
617
+ ]
535
618
  }
536
619
  ),
537
- failure ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { fontSize: 12, marginTop: 8 }, children: failure === "popup-blocked" /* PopupBlocked */ ? "Your browser blocked the sign-in popup. Allow popups for this site and try again." : "Sign-in didn't complete. Please try again." }) : null
538
- ] })
620
+ failure ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { fontSize: 12.5, lineHeight: 1.5, marginTop: 14, color: "#a1a1aa" }, children: failure === "popup-blocked" /* PopupBlocked */ ? "Your browser blocked the sign-in popup. Allow popups for this site and try again." : "Sign-in didn't complete. Please try again." }) : null
621
+ ]
539
622
  }
540
- );
623
+ ) });
541
624
  }
542
625
  function AuthGate({ children, signInScreen, loader }) {
543
626
  const [isCallback, setIsCallback] = (0, import_react2.useState)(false);
@@ -703,6 +786,11 @@ function createHostAuth(config, silentAuth) {
703
786
  for (const listener of [...seedListeners]) listener(principal);
704
787
  }
705
788
  function wireHostAuth(bridge) {
789
+ if (typeof bridge?.onAuthChange !== "function") {
790
+ throw new TypeError(
791
+ "[vibe-auth] wireHostAuth(bridge) requires a VibeAppBridge \u2014 the object passed has no onAuthChange. Prefer wireVibeApp(bridge), which performs the whole handshake."
792
+ );
793
+ }
706
794
  hostWired = true;
707
795
  return bridge.onAuthChange((auth) => {
708
796
  if (!auth.authenticated) {
@@ -733,6 +821,15 @@ function createHostAuth(config, silentAuth) {
733
821
  }
734
822
 
735
823
  // src/wireVibeApp.ts
824
+ function classifyConnectFailure(error) {
825
+ const code = error?.code;
826
+ if (code === "PARENT_ORIGIN_UNRESOLVED") return { kind: "standalone" };
827
+ if (code === "TIMEOUT") {
828
+ const framed = typeof window !== "undefined" && window.parent !== window;
829
+ return framed ? { kind: "failed", error } : { kind: "standalone" };
830
+ }
831
+ return { kind: "failed", error };
832
+ }
736
833
  function createWireVibeApp(config, hostAuth) {
737
834
  return function wireVibeApp(bridge, options = {}) {
738
835
  const listeners = /* @__PURE__ */ new Set();
@@ -748,21 +845,31 @@ function createWireVibeApp(config, hostAuth) {
748
845
  if (options.onSubrouteRequest) bridge.onSubrouteRequest?.(options.onSubrouteRequest);
749
846
  hostAuth.wireHostAuth(bridge);
750
847
  const noHost = typeof window === "undefined" || window.location.pathname === config.callbackPath;
751
- const hostContext = noHost ? Promise.resolve(null) : bridge.connect().then((ctx) => {
848
+ let settleOutcome;
849
+ const hostOutcome = new Promise((resolve) => {
850
+ settleOutcome = resolve;
851
+ });
852
+ const hostContext = noHost ? (settleOutcome({ kind: "standalone" }), Promise.resolve(null)) : bridge.connect().then((ctx) => {
752
853
  const next = ctx;
753
854
  hostAuth.seedLastUserId(next.user.id, next.tenant);
754
855
  if (next.enableDataReset && options.onDataReset) {
755
856
  bridge.onDataReset?.(options.onDataReset);
756
857
  }
757
858
  publish(next);
859
+ settleOutcome({ kind: "connected" });
758
860
  return next;
759
861
  }).catch((error) => {
760
- console.warn("[vibe-auth] host connect failed", error);
862
+ const outcome = classifyConnectFailure(error);
863
+ if (outcome.kind === "failed") {
864
+ console.warn("[vibe-auth] host connect failed", error);
865
+ }
866
+ settleOutcome(outcome);
761
867
  return null;
762
868
  });
763
869
  return {
764
870
  bridge,
765
871
  hostContext,
872
+ hostOutcome,
766
873
  getHostContext: () => current,
767
874
  subscribeHostContext: (listener) => {
768
875
  listeners.add(listener);