@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/index.d.cts CHANGED
@@ -465,16 +465,47 @@ interface CloudStorageOptions {
465
465
  apiUrl?: string;
466
466
  /** Open the sign-in popup automatically on the first auth-needed write. Defaults to true. */
467
467
  autoSignIn?: boolean;
468
+ /**
469
+ * Fired whenever the cached host-token transitions to authenticated —
470
+ * either freshly from the popup or rehydrated from sessionStorage.
471
+ * Vizu wires this to `setUser()` so comments attributed after sign-in
472
+ * carry the proper display name + avatar.
473
+ */
474
+ onAuthChanged?: (info: {
475
+ userId: string;
476
+ user: VizuUser | null;
477
+ }) => void;
468
478
  }
469
479
  declare class CloudStorageAdapter implements StorageAdapter {
470
480
  private readonly apiUrl;
471
481
  private readonly workspace;
472
482
  private readonly autoSignIn;
483
+ private onAuthChanged;
473
484
  /** Cached current-tab token. Mirrored to sessionStorage. */
474
485
  private cachedToken;
475
486
  /** Single-flight: at most one popup at a time. Subsequent requests await this. */
476
487
  private pendingAuth;
488
+ /** Whether we've already fired onAuthChanged for the current token. */
489
+ private lastNotifiedTokenId;
477
490
  constructor(opts: CloudStorageOptions);
491
+ /**
492
+ * Open the sign-in popup right now (or no-op if a valid token is
493
+ * cached). Vizu calls this on enable() so the user signs in BEFORE
494
+ * trying to leave a comment — no surprise modal mid-comment.
495
+ *
496
+ * Throws auth_canceled if the user closes the popup. Caller can
497
+ * decide whether to disable the surface or leave it in a read-only
498
+ * state.
499
+ */
500
+ preflightAuth(): Promise<void>;
501
+ /**
502
+ * Synchronous accessor for the currently cached bearer token, used
503
+ * by sibling modules that need to authenticate against the same
504
+ * backend (live-collab cross-origin polling). Returns null when no
505
+ * valid token is cached — callers fall back to credentials:'include'
506
+ * for same-origin or just disable themselves.
507
+ */
508
+ getCachedAuthToken(): string | null;
478
509
  load(_namespace: string): Promise<VizuComment[]>;
479
510
  addComment(_namespace: string, comment: VizuComment): Promise<void>;
480
511
  updateComment(_namespace: string, id: string, patch: Partial<VizuComment>): Promise<VizuComment | null>;
@@ -512,6 +543,12 @@ declare class CloudStorageAdapter implements StorageAdapter {
512
543
  * a friendly message instead of a blank failure.
513
544
  */
514
545
  private resolveToken;
546
+ /**
547
+ * Notify the host that the authenticated identity changed. De-duped
548
+ * via the token string so multiple resolveToken() hits with the same
549
+ * cached token don't re-call setUser on every fetch.
550
+ */
551
+ private fireAuthChanged;
515
552
  private openSignInPopup;
516
553
  /**
517
554
  * Perform an authenticated fetch with one-shot retry on 401 (re-opens
package/dist/index.d.ts CHANGED
@@ -465,16 +465,47 @@ interface CloudStorageOptions {
465
465
  apiUrl?: string;
466
466
  /** Open the sign-in popup automatically on the first auth-needed write. Defaults to true. */
467
467
  autoSignIn?: boolean;
468
+ /**
469
+ * Fired whenever the cached host-token transitions to authenticated —
470
+ * either freshly from the popup or rehydrated from sessionStorage.
471
+ * Vizu wires this to `setUser()` so comments attributed after sign-in
472
+ * carry the proper display name + avatar.
473
+ */
474
+ onAuthChanged?: (info: {
475
+ userId: string;
476
+ user: VizuUser | null;
477
+ }) => void;
468
478
  }
469
479
  declare class CloudStorageAdapter implements StorageAdapter {
470
480
  private readonly apiUrl;
471
481
  private readonly workspace;
472
482
  private readonly autoSignIn;
483
+ private onAuthChanged;
473
484
  /** Cached current-tab token. Mirrored to sessionStorage. */
474
485
  private cachedToken;
475
486
  /** Single-flight: at most one popup at a time. Subsequent requests await this. */
476
487
  private pendingAuth;
488
+ /** Whether we've already fired onAuthChanged for the current token. */
489
+ private lastNotifiedTokenId;
477
490
  constructor(opts: CloudStorageOptions);
491
+ /**
492
+ * Open the sign-in popup right now (or no-op if a valid token is
493
+ * cached). Vizu calls this on enable() so the user signs in BEFORE
494
+ * trying to leave a comment — no surprise modal mid-comment.
495
+ *
496
+ * Throws auth_canceled if the user closes the popup. Caller can
497
+ * decide whether to disable the surface or leave it in a read-only
498
+ * state.
499
+ */
500
+ preflightAuth(): Promise<void>;
501
+ /**
502
+ * Synchronous accessor for the currently cached bearer token, used
503
+ * by sibling modules that need to authenticate against the same
504
+ * backend (live-collab cross-origin polling). Returns null when no
505
+ * valid token is cached — callers fall back to credentials:'include'
506
+ * for same-origin or just disable themselves.
507
+ */
508
+ getCachedAuthToken(): string | null;
478
509
  load(_namespace: string): Promise<VizuComment[]>;
479
510
  addComment(_namespace: string, comment: VizuComment): Promise<void>;
480
511
  updateComment(_namespace: string, id: string, patch: Partial<VizuComment>): Promise<VizuComment | null>;
@@ -512,6 +543,12 @@ declare class CloudStorageAdapter implements StorageAdapter {
512
543
  * a friendly message instead of a blank failure.
513
544
  */
514
545
  private resolveToken;
546
+ /**
547
+ * Notify the host that the authenticated identity changed. De-duped
548
+ * via the token string so multiple resolveToken() hits with the same
549
+ * cached token don't re-call setUser on every fetch.
550
+ */
551
+ private fireAuthChanged;
515
552
  private openSignInPopup;
516
553
  /**
517
554
  * Perform an authenticated fetch with one-shot retry on 401 (re-opens
package/dist/index.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;