@unhingged/vizu-core 0.1.0 → 0.1.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/auto.js CHANGED
@@ -342,10 +342,40 @@ var CloudStorageAdapter = class {
342
342
  this.cachedToken = null;
343
343
  /** Single-flight: at most one popup at a time. Subsequent requests await this. */
344
344
  this.pendingAuth = null;
345
+ /** Whether we've already fired onAuthChanged for the current token. */
346
+ this.lastNotifiedTokenId = null;
345
347
  this.workspace = opts.workspace;
346
348
  this.apiUrl = (opts.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, "");
347
349
  this.autoSignIn = opts.autoSignIn !== false;
350
+ this.onAuthChanged = opts.onAuthChanged;
348
351
  this.cachedToken = this.readStoredToken();
352
+ if (this.cachedToken && !this.isExpired(this.cachedToken)) {
353
+ this.fireAuthChanged(this.cachedToken);
354
+ }
355
+ }
356
+ /**
357
+ * Open the sign-in popup right now (or no-op if a valid token is
358
+ * cached). Vizu calls this on enable() so the user signs in BEFORE
359
+ * trying to leave a comment — no surprise modal mid-comment.
360
+ *
361
+ * Throws auth_canceled if the user closes the popup. Caller can
362
+ * decide whether to disable the surface or leave it in a read-only
363
+ * state.
364
+ */
365
+ async preflightAuth() {
366
+ await this.resolveToken();
367
+ }
368
+ /**
369
+ * Synchronous accessor for the currently cached bearer token, used
370
+ * by sibling modules that need to authenticate against the same
371
+ * backend (live-collab cross-origin polling). Returns null when no
372
+ * valid token is cached — callers fall back to credentials:'include'
373
+ * for same-origin or just disable themselves.
374
+ */
375
+ getCachedAuthToken() {
376
+ if (!this.cachedToken) return null;
377
+ if (this.isExpired(this.cachedToken)) return null;
378
+ return this.cachedToken.token;
349
379
  }
350
380
  /* ─── StorageAdapter v2 ───────────────────────────────────────────── */
351
381
  async load(_namespace) {
@@ -519,7 +549,7 @@ var CloudStorageAdapter = class {
519
549
  }
520
550
  }
521
551
  sessionKey() {
522
- return `vizu:cloud-token:${this.workspace}`;
552
+ return `vizu:cloud-token:v2:${this.workspace}`;
523
553
  }
524
554
  /**
525
555
  * Resolve a valid token, opening the popup if needed.
@@ -534,6 +564,7 @@ var CloudStorageAdapter = class {
534
564
  const fresh = this.readStoredToken();
535
565
  if (fresh) {
536
566
  this.cachedToken = fresh;
567
+ this.fireAuthChanged(fresh);
537
568
  return fresh;
538
569
  }
539
570
  if (!this.autoSignIn) {
@@ -542,6 +573,7 @@ var CloudStorageAdapter = class {
542
573
  if (!this.pendingAuth) {
543
574
  this.pendingAuth = this.openSignInPopup().then((t) => {
544
575
  this.writeStoredToken(t);
576
+ this.fireAuthChanged(t);
545
577
  return t;
546
578
  }).finally(() => {
547
579
  this.pendingAuth = null;
@@ -549,6 +581,22 @@ var CloudStorageAdapter = class {
549
581
  }
550
582
  return this.pendingAuth;
551
583
  }
584
+ /**
585
+ * Notify the host that the authenticated identity changed. De-duped
586
+ * via the token string so multiple resolveToken() hits with the same
587
+ * cached token don't re-call setUser on every fetch.
588
+ */
589
+ fireAuthChanged(token) {
590
+ if (this.lastNotifiedTokenId === token.token) return;
591
+ this.lastNotifiedTokenId = token.token;
592
+ try {
593
+ this.onAuthChanged?.({ userId: token.userId, user: token.user });
594
+ } catch (err) {
595
+ if (typeof console !== "undefined") {
596
+ console.warn("[vizu/cloud] onAuthChanged callback threw", err);
597
+ }
598
+ }
599
+ }
552
600
  openSignInPopup() {
553
601
  return new Promise((resolve, reject) => {
554
602
  if (typeof window === "undefined") {
@@ -581,7 +629,13 @@ var CloudStorageAdapter = class {
581
629
  if (typeof data.token !== "string" || typeof data.expiresAt !== "string" || typeof data.userId !== "string") return;
582
630
  resolved = true;
583
631
  cleanup();
584
- resolve({ token: data.token, expiresAt: data.expiresAt, userId: data.userId });
632
+ const userPayload = data.user;
633
+ const user = userPayload && typeof userPayload.name === "string" ? {
634
+ id: typeof userPayload.id === "string" ? userPayload.id : data.userId,
635
+ name: userPayload.name,
636
+ avatarUrl: typeof userPayload.avatarUrl === "string" ? userPayload.avatarUrl : void 0
637
+ } : null;
638
+ resolve({ token: data.token, expiresAt: data.expiresAt, userId: data.userId, user });
585
639
  };
586
640
  const onPoll = window.setInterval(() => {
587
641
  if (popup.closed) {
@@ -2676,7 +2730,7 @@ var DEFAULTS = {
2676
2730
  namespace: "default",
2677
2731
  accent: "#FF6647"
2678
2732
  };
2679
- function resolveStorage(storage, cloud, defaultMode) {
2733
+ function resolveStorage(storage, cloud, defaultMode, onAuthChanged) {
2680
2734
  if (cloud) {
2681
2735
  if (storage && typeof console !== "undefined") {
2682
2736
  console.warn("[vizu] Both `cloud` and `storage` set; `cloud` wins. Drop `storage` to silence this.");
@@ -2684,7 +2738,8 @@ function resolveStorage(storage, cloud, defaultMode) {
2684
2738
  return new CloudStorageAdapter({
2685
2739
  workspace: cloud.workspace,
2686
2740
  apiUrl: cloud.apiUrl,
2687
- autoSignIn: cloud.autoSignIn
2741
+ autoSignIn: cloud.autoSignIn,
2742
+ onAuthChanged
2688
2743
  });
2689
2744
  }
2690
2745
  if (!storage) return defaultMode === "local" ? new LocalStorageAdapter() : new InMemoryStorageAdapter();
@@ -2790,19 +2845,39 @@ var Vizu = class {
2790
2845
  this.opts.namespace = options.namespace ?? DEFAULTS.namespace;
2791
2846
  this.opts.accent = options.accent ?? DEFAULTS.accent;
2792
2847
  this.parsedShortcut = parseShortcut(this.opts.shortcut);
2793
- this.storage = resolveStorage(options.storage, options.cloud, _defaultStorage);
2848
+ this.storage = resolveStorage(
2849
+ options.storage,
2850
+ options.cloud,
2851
+ _defaultStorage,
2852
+ (info) => {
2853
+ if (info.user) this.setUser(info.user);
2854
+ }
2855
+ );
2794
2856
  this.user = options.user ?? null;
2795
2857
  if (options.actions) this.actions = [...options.actions];
2796
2858
  if (options.onCommentAdded) this.bus.on("comment:added", (p) => options.onCommentAdded(p.comment));
2797
2859
  if (options.onCommentRemoved) this.bus.on("comment:removed", (p) => options.onCommentRemoved(p.id));
2798
- if (options.liveblocks && options.cloud) {
2799
- const live = options.liveblocks;
2860
+ if (options.cloud) {
2800
2861
  const cloud = options.cloud;
2801
- void import("./live-collab-IRUNFAPE.js").then(({ LiveCollab }) => {
2862
+ const apiUrl = (cloud.apiUrl ?? "https://vizu.unhingged.com").replace(/\/$/, "");
2863
+ void import("./live-collab-BVLNJ5QI.js").then(({ LiveCollab }) => {
2802
2864
  this.liveCollab = new LiveCollab({
2803
- publicKey: live.publicKey,
2804
- authEndpoint: live.authEndpoint,
2805
- workspaceSlug: cloud.workspace
2865
+ workspaceSlug: cloud.workspace,
2866
+ apiUrl,
2867
+ getAuthHeader: async () => {
2868
+ if (typeof window === "undefined") return null;
2869
+ try {
2870
+ if (new URL(apiUrl).origin === window.location.origin) return null;
2871
+ } catch {
2872
+ return null;
2873
+ }
2874
+ const adapter = this.storage;
2875
+ if (adapter instanceof CloudStorageAdapter) {
2876
+ const token = adapter.getCachedAuthToken();
2877
+ if (token) return { Authorization: `Bearer ${token}` };
2878
+ }
2879
+ return null;
2880
+ }
2806
2881
  });
2807
2882
  void this.liveCollab.start();
2808
2883
  });
@@ -2836,6 +2911,14 @@ var Vizu = class {
2836
2911
  this.enabled = true;
2837
2912
  this.bus.emit("enabled", {});
2838
2913
  this.deferred(() => this.mount());
2914
+ if (this.storage instanceof CloudStorageAdapter) {
2915
+ void this.storage.preflightAuth().catch((err) => {
2916
+ if (err?.code === "auth_canceled" || err?.code === "popup_blocked") return;
2917
+ if (typeof console !== "undefined") {
2918
+ console.warn("[vizu] preflight auth failed:", err);
2919
+ }
2920
+ });
2921
+ }
2839
2922
  }
2840
2923
  disable() {
2841
2924
  if (!this.enabled) return;