@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 +6 -1
- package/README.md +14 -1
- package/dist/index.cjs +121 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +70 -10
- package/dist/index.d.ts +70 -10
- package/dist/index.js +121 -14
- package/dist/index.js.map +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
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: "
|
|
509
|
-
|
|
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: {
|
|
520
|
-
|
|
521
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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);
|