@nominalso/vibe-auth 0.2.4 → 0.3.0
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 +5 -0
- package/README.md +13 -0
- package/dist/index.cjs +121 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +65 -3
- package/dist/index.d.ts +65 -3
- package/dist/index.js +121 -14
- package/dist/index.js.map +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -202,9 +202,34 @@ interface InteractiveAuth {
|
|
|
202
202
|
signInInteractive(): Promise<SignInResult>;
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
/**
|
|
205
|
+
/**
|
|
206
|
+
* The subset of `VibeAppBridge` this module needs — structural, not a hard
|
|
207
|
+
* dependency.
|
|
208
|
+
*
|
|
209
|
+
* `onAuthChange` is **optional in the type and required at runtime**, which
|
|
210
|
+
* looks wrong until you see what forced it: the method is `@internal` in
|
|
211
|
+
* `@nominalso/vibe-bridge` and `stripInternal` removes it from that package's
|
|
212
|
+
* published `.d.ts`. A real `VibeAppBridge` therefore does not structurally
|
|
213
|
+
* satisfy a type that demands it, and `wireHostAuth(bridge)` stopped
|
|
214
|
+
* compiling for every app on bridge >= 0.12 — the exact combination the
|
|
215
|
+
* compatibility promise covers.
|
|
216
|
+
*
|
|
217
|
+
* Optional makes the call compile again; `wireHostAuth` then throws if the
|
|
218
|
+
* method really is absent, so the mistake is loud rather than a silent no-op
|
|
219
|
+
* that drops host-session lockstep (a user staying signed in after a Nominal
|
|
220
|
+
* logout, which is not a failure to discover in production).
|
|
221
|
+
*/
|
|
206
222
|
interface AuthBridgeLike {
|
|
207
|
-
|
|
223
|
+
/**
|
|
224
|
+
* Not used here — it anchors the type. With `onAuthChange` optional this
|
|
225
|
+
* interface would have no required member, making it a *weak type*: TypeScript
|
|
226
|
+
* then demands the argument share at least one property, and a `VibeAppBridge`
|
|
227
|
+
* (whose `onAuthChange` is stripped) shares none, so the call still would not
|
|
228
|
+
* compile. `connect` is public on every bridge, so requiring it fixes that and
|
|
229
|
+
* says what we actually mean — "this is a bridge".
|
|
230
|
+
*/
|
|
231
|
+
connect(): Promise<unknown>;
|
|
232
|
+
onAuthChange?(cb: (auth: {
|
|
208
233
|
authenticated: boolean;
|
|
209
234
|
userId?: string;
|
|
210
235
|
tenant?: string;
|
|
@@ -322,6 +347,30 @@ interface WireVibeAppOptions<Ctx extends HostContextLike> {
|
|
|
322
347
|
*/
|
|
323
348
|
onSubrouteRequest?: (subroute: string) => void;
|
|
324
349
|
}
|
|
350
|
+
/**
|
|
351
|
+
* Why the handshake ended the way it did.
|
|
352
|
+
*
|
|
353
|
+
* `hostContext` resolving `null` is deliberately not an error — a standalone
|
|
354
|
+
* preview is the normal way to run a Vibe App outside Nominal. But it collapses
|
|
355
|
+
* two situations an app usually wants to treat differently:
|
|
356
|
+
*
|
|
357
|
+
* - **`standalone`** — nothing was ever going to answer (not embedded, or the
|
|
358
|
+
* OAuth callback document). Expected. Render the "open this from Nominal"
|
|
359
|
+
* state, quietly.
|
|
360
|
+
* - **`failed`** — the app *is* embedded and the host did not answer. A real
|
|
361
|
+
* fault worth surfacing, with `error` carrying the underlying `BridgeError`.
|
|
362
|
+
*
|
|
363
|
+
* Without this, an app that shows an error on a null context cries wolf on
|
|
364
|
+
* every dev load, and one that stays silent hides genuine breakage.
|
|
365
|
+
*/
|
|
366
|
+
type HostOutcome = {
|
|
367
|
+
kind: 'connected';
|
|
368
|
+
} | {
|
|
369
|
+
kind: 'standalone';
|
|
370
|
+
} | {
|
|
371
|
+
kind: 'failed';
|
|
372
|
+
error: unknown;
|
|
373
|
+
};
|
|
325
374
|
interface VibeAppWiring<Ctx extends HostContextLike> {
|
|
326
375
|
/** The bridge you passed in, returned so one import covers the whole module. */
|
|
327
376
|
bridge: VibeBridgeLike;
|
|
@@ -332,6 +381,19 @@ interface VibeAppWiring<Ctx extends HostContextLike> {
|
|
|
332
381
|
* to interactive sign-in on its own.
|
|
333
382
|
*/
|
|
334
383
|
hostContext: Promise<Ctx | null>;
|
|
384
|
+
/**
|
|
385
|
+
* Why the handshake ended as it did — the distinction `hostContext`'s `null`
|
|
386
|
+
* cannot carry. Settles at the same time as `hostContext`, and like it,
|
|
387
|
+
* **never rejects**.
|
|
388
|
+
*
|
|
389
|
+
* @example
|
|
390
|
+
* ```ts
|
|
391
|
+
* const outcome = await hostOutcome
|
|
392
|
+
* if (outcome.kind === 'failed') showError(outcome.error) // embedded, host silent
|
|
393
|
+
* else if (outcome.kind === 'standalone') showOpenFromNominal()
|
|
394
|
+
* ```
|
|
395
|
+
*/
|
|
396
|
+
hostOutcome: Promise<HostOutcome>;
|
|
335
397
|
/** Current host context; `null` before connect resolves and when standalone. */
|
|
336
398
|
getHostContext(): Ctx | null;
|
|
337
399
|
/** Subscribe to context changes. Returns an unsubscribe function. */
|
|
@@ -452,4 +514,4 @@ declare function createVibeAuth(userConfig: VibeAuthConfig): VibeAuth;
|
|
|
452
514
|
|
|
453
515
|
declare const RESULT_MESSAGE_TYPE = "silent-auth-result";
|
|
454
516
|
|
|
455
|
-
export { type AuthBridgeLike, type AuthGateProps, type AuthResult, AuthResultKind, type HostContextLike, type HostPrincipal, RESULT_MESSAGE_TYPE, type ResetPayloadLike, type ResolvedVibeAuthConfig, type ResolvedVibeAuthTimeouts, type SignInAuthenticated, type SignInFailed, SignInFailureReason, SignInKind, type SignInRedirecting, type SignInResult, type VibeAppWiring, type VibeAuth, type VibeAuthConfig, type VibeAuthTimeouts, type VibeBridgeLike, type WireVibeAppOptions, createVibeAuth };
|
|
517
|
+
export { type AuthBridgeLike, type AuthGateProps, type AuthResult, AuthResultKind, type HostContextLike, type HostOutcome, type HostPrincipal, RESULT_MESSAGE_TYPE, type ResetPayloadLike, type ResolvedVibeAuthConfig, type ResolvedVibeAuthTimeouts, type SignInAuthenticated, type SignInFailed, SignInFailureReason, SignInKind, type SignInRedirecting, type SignInResult, type VibeAppWiring, type VibeAuth, type VibeAuthConfig, type VibeAuthTimeouts, type VibeBridgeLike, type WireVibeAppOptions, createVibeAuth };
|
package/dist/index.js
CHANGED
|
@@ -472,28 +472,108 @@ function createAuthGate(config, silentAuth, interactiveAuth, hostAuth) {
|
|
|
472
472
|
const { supabase, callbackPath, timeouts } = config;
|
|
473
473
|
const RETURNING_FROM_OAUTH = typeof window !== "undefined" && (new URLSearchParams(window.location.search).has("code") || window.location.hash.includes("access_token"));
|
|
474
474
|
const IS_CALLBACK_PATH = typeof window !== "undefined" && window.location.pathname === callbackPath;
|
|
475
|
+
const SPIN_CLASS = "nominal-vibe-auth-spin";
|
|
476
|
+
function Spinner({ size = 18 }) {
|
|
477
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
478
|
+
/* @__PURE__ */ jsx("style", { children: `
|
|
479
|
+
@keyframes ${SPIN_CLASS} { to { transform: rotate(360deg) } }
|
|
480
|
+
.${SPIN_CLASS} { animation: ${SPIN_CLASS} 700ms linear infinite; transform-origin: 50% 50% }
|
|
481
|
+
@media (prefers-reduced-motion: reduce) { .${SPIN_CLASS} { animation-duration: 2.4s } }
|
|
482
|
+
` }),
|
|
483
|
+
/* @__PURE__ */ jsxs(
|
|
484
|
+
"svg",
|
|
485
|
+
{
|
|
486
|
+
className: SPIN_CLASS,
|
|
487
|
+
width: size,
|
|
488
|
+
height: size,
|
|
489
|
+
viewBox: "0 0 24 24",
|
|
490
|
+
fill: "none",
|
|
491
|
+
"aria-hidden": "true",
|
|
492
|
+
children: [
|
|
493
|
+
/* @__PURE__ */ jsx(
|
|
494
|
+
"circle",
|
|
495
|
+
{
|
|
496
|
+
cx: "12",
|
|
497
|
+
cy: "12",
|
|
498
|
+
r: "9",
|
|
499
|
+
stroke: "currentColor",
|
|
500
|
+
strokeOpacity: "0.18",
|
|
501
|
+
strokeWidth: "3"
|
|
502
|
+
}
|
|
503
|
+
),
|
|
504
|
+
/* @__PURE__ */ jsx(
|
|
505
|
+
"path",
|
|
506
|
+
{
|
|
507
|
+
d: "M21 12a9 9 0 0 0-9-9",
|
|
508
|
+
stroke: "currentColor",
|
|
509
|
+
strokeWidth: "3",
|
|
510
|
+
strokeLinecap: "round"
|
|
511
|
+
}
|
|
512
|
+
)
|
|
513
|
+
]
|
|
514
|
+
}
|
|
515
|
+
)
|
|
516
|
+
] });
|
|
517
|
+
}
|
|
518
|
+
const SCREEN = {
|
|
519
|
+
display: "grid",
|
|
520
|
+
placeItems: "center",
|
|
521
|
+
height: "100vh",
|
|
522
|
+
fontFamily: 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
|
|
523
|
+
color: "#3f3f46",
|
|
524
|
+
background: "#fafafa"
|
|
525
|
+
};
|
|
475
526
|
function DefaultFullScreenLoader() {
|
|
476
|
-
return /* @__PURE__ */ jsx(
|
|
527
|
+
return /* @__PURE__ */ jsx("div", { style: SCREEN, children: /* @__PURE__ */ jsxs(
|
|
477
528
|
"div",
|
|
478
529
|
{
|
|
479
|
-
style: { display: "
|
|
480
|
-
|
|
530
|
+
style: { display: "flex", alignItems: "center", gap: 10, fontSize: 14 },
|
|
531
|
+
role: "status",
|
|
532
|
+
"aria-live": "polite",
|
|
533
|
+
children: [
|
|
534
|
+
/* @__PURE__ */ jsx(Spinner, {}),
|
|
535
|
+
/* @__PURE__ */ jsx("span", { children: "Fetching your data\u2026" })
|
|
536
|
+
]
|
|
481
537
|
}
|
|
482
|
-
);
|
|
538
|
+
) });
|
|
483
539
|
}
|
|
484
540
|
function DefaultSignInScreen() {
|
|
485
541
|
const [pending, setPending] = useState(false);
|
|
486
542
|
const [failure, setFailure] = useState(null);
|
|
487
|
-
return /* @__PURE__ */ jsx(
|
|
543
|
+
return /* @__PURE__ */ jsx("div", { style: SCREEN, children: /* @__PURE__ */ jsxs(
|
|
488
544
|
"div",
|
|
489
545
|
{
|
|
490
|
-
style: {
|
|
491
|
-
|
|
492
|
-
|
|
546
|
+
style: {
|
|
547
|
+
textAlign: "center",
|
|
548
|
+
background: "#fff",
|
|
549
|
+
border: "1px solid #e4e4e7",
|
|
550
|
+
borderRadius: 12,
|
|
551
|
+
padding: "28px 32px",
|
|
552
|
+
boxShadow: "0 1px 2px rgba(0,0,0,0.04)",
|
|
553
|
+
maxWidth: 340
|
|
554
|
+
},
|
|
555
|
+
children: [
|
|
556
|
+
/* @__PURE__ */ jsxs(
|
|
493
557
|
"button",
|
|
494
558
|
{
|
|
495
559
|
type: "button",
|
|
496
560
|
disabled: pending,
|
|
561
|
+
style: {
|
|
562
|
+
display: "inline-flex",
|
|
563
|
+
alignItems: "center",
|
|
564
|
+
justifyContent: "center",
|
|
565
|
+
gap: 8,
|
|
566
|
+
minWidth: 168,
|
|
567
|
+
padding: "10px 20px",
|
|
568
|
+
fontFamily: "inherit",
|
|
569
|
+
fontSize: 14,
|
|
570
|
+
fontWeight: 500,
|
|
571
|
+
color: "#fff",
|
|
572
|
+
background: pending ? "#71717a" : "#18181b",
|
|
573
|
+
border: "none",
|
|
574
|
+
borderRadius: 8,
|
|
575
|
+
cursor: pending ? "default" : "pointer"
|
|
576
|
+
},
|
|
497
577
|
onClick: () => {
|
|
498
578
|
setPending(true);
|
|
499
579
|
setFailure(null);
|
|
@@ -502,13 +582,16 @@ function createAuthGate(config, silentAuth, interactiveAuth, hostAuth) {
|
|
|
502
582
|
if (result.kind !== "redirecting" /* Redirecting */) setPending(false);
|
|
503
583
|
});
|
|
504
584
|
},
|
|
505
|
-
children:
|
|
585
|
+
children: [
|
|
586
|
+
pending ? /* @__PURE__ */ jsx(Spinner, { size: 15 }) : null,
|
|
587
|
+
pending ? "Opening sign-in\u2026" : "Sign in"
|
|
588
|
+
]
|
|
506
589
|
}
|
|
507
590
|
),
|
|
508
|
-
failure ? /* @__PURE__ */ jsx("p", { style: { fontSize: 12, marginTop:
|
|
509
|
-
]
|
|
591
|
+
failure ? /* @__PURE__ */ 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
|
|
592
|
+
]
|
|
510
593
|
}
|
|
511
|
-
);
|
|
594
|
+
) });
|
|
512
595
|
}
|
|
513
596
|
function AuthGate({ children, signInScreen, loader }) {
|
|
514
597
|
const [isCallback, setIsCallback] = useState(false);
|
|
@@ -674,6 +757,11 @@ function createHostAuth(config, silentAuth) {
|
|
|
674
757
|
for (const listener of [...seedListeners]) listener(principal);
|
|
675
758
|
}
|
|
676
759
|
function wireHostAuth(bridge) {
|
|
760
|
+
if (typeof bridge?.onAuthChange !== "function") {
|
|
761
|
+
throw new TypeError(
|
|
762
|
+
"[vibe-auth] wireHostAuth(bridge) requires a VibeAppBridge \u2014 the object passed has no onAuthChange. Prefer wireVibeApp(bridge), which performs the whole handshake."
|
|
763
|
+
);
|
|
764
|
+
}
|
|
677
765
|
hostWired = true;
|
|
678
766
|
return bridge.onAuthChange((auth) => {
|
|
679
767
|
if (!auth.authenticated) {
|
|
@@ -704,6 +792,15 @@ function createHostAuth(config, silentAuth) {
|
|
|
704
792
|
}
|
|
705
793
|
|
|
706
794
|
// src/wireVibeApp.ts
|
|
795
|
+
function classifyConnectFailure(error) {
|
|
796
|
+
const code = error?.code;
|
|
797
|
+
if (code === "PARENT_ORIGIN_UNRESOLVED") return { kind: "standalone" };
|
|
798
|
+
if (code === "TIMEOUT") {
|
|
799
|
+
const framed = typeof window !== "undefined" && window.parent !== window;
|
|
800
|
+
return framed ? { kind: "failed", error } : { kind: "standalone" };
|
|
801
|
+
}
|
|
802
|
+
return { kind: "failed", error };
|
|
803
|
+
}
|
|
707
804
|
function createWireVibeApp(config, hostAuth) {
|
|
708
805
|
return function wireVibeApp(bridge, options = {}) {
|
|
709
806
|
const listeners = /* @__PURE__ */ new Set();
|
|
@@ -719,21 +816,31 @@ function createWireVibeApp(config, hostAuth) {
|
|
|
719
816
|
if (options.onSubrouteRequest) bridge.onSubrouteRequest?.(options.onSubrouteRequest);
|
|
720
817
|
hostAuth.wireHostAuth(bridge);
|
|
721
818
|
const noHost = typeof window === "undefined" || window.location.pathname === config.callbackPath;
|
|
722
|
-
|
|
819
|
+
let settleOutcome;
|
|
820
|
+
const hostOutcome = new Promise((resolve) => {
|
|
821
|
+
settleOutcome = resolve;
|
|
822
|
+
});
|
|
823
|
+
const hostContext = noHost ? (settleOutcome({ kind: "standalone" }), Promise.resolve(null)) : bridge.connect().then((ctx) => {
|
|
723
824
|
const next = ctx;
|
|
724
825
|
hostAuth.seedLastUserId(next.user.id, next.tenant);
|
|
725
826
|
if (next.enableDataReset && options.onDataReset) {
|
|
726
827
|
bridge.onDataReset?.(options.onDataReset);
|
|
727
828
|
}
|
|
728
829
|
publish(next);
|
|
830
|
+
settleOutcome({ kind: "connected" });
|
|
729
831
|
return next;
|
|
730
832
|
}).catch((error) => {
|
|
731
|
-
|
|
833
|
+
const outcome = classifyConnectFailure(error);
|
|
834
|
+
if (outcome.kind === "failed") {
|
|
835
|
+
console.warn("[vibe-auth] host connect failed", error);
|
|
836
|
+
}
|
|
837
|
+
settleOutcome(outcome);
|
|
732
838
|
return null;
|
|
733
839
|
});
|
|
734
840
|
return {
|
|
735
841
|
bridge,
|
|
736
842
|
hostContext,
|
|
843
|
+
hostOutcome,
|
|
737
844
|
getHostContext: () => current,
|
|
738
845
|
subscribeHostContext: (listener) => {
|
|
739
846
|
listeners.add(listener);
|