@unhingged/vizu-core 0.1.0 → 0.1.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/dist/index.d.cts CHANGED
@@ -465,16 +465,39 @@ 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>;
478
501
  load(_namespace: string): Promise<VizuComment[]>;
479
502
  addComment(_namespace: string, comment: VizuComment): Promise<void>;
480
503
  updateComment(_namespace: string, id: string, patch: Partial<VizuComment>): Promise<VizuComment | null>;
@@ -512,6 +535,12 @@ declare class CloudStorageAdapter implements StorageAdapter {
512
535
  * a friendly message instead of a blank failure.
513
536
  */
514
537
  private resolveToken;
538
+ /**
539
+ * Notify the host that the authenticated identity changed. De-duped
540
+ * via the token string so multiple resolveToken() hits with the same
541
+ * cached token don't re-call setUser on every fetch.
542
+ */
543
+ private fireAuthChanged;
515
544
  private openSignInPopup;
516
545
  /**
517
546
  * Perform an authenticated fetch with one-shot retry on 401 (re-opens
package/dist/index.d.ts CHANGED
@@ -465,16 +465,39 @@ 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>;
478
501
  load(_namespace: string): Promise<VizuComment[]>;
479
502
  addComment(_namespace: string, comment: VizuComment): Promise<void>;
480
503
  updateComment(_namespace: string, id: string, patch: Partial<VizuComment>): Promise<VizuComment | null>;
@@ -512,6 +535,12 @@ declare class CloudStorageAdapter implements StorageAdapter {
512
535
  * a friendly message instead of a blank failure.
513
536
  */
514
537
  private resolveToken;
538
+ /**
539
+ * Notify the host that the authenticated identity changed. De-duped
540
+ * via the token string so multiple resolveToken() hits with the same
541
+ * cached token don't re-call setUser on every fetch.
542
+ */
543
+ private fireAuthChanged;
515
544
  private openSignInPopup;
516
545
  /**
517
546
  * Perform an authenticated fetch with one-shot retry on 401 (re-opens
package/dist/index.js CHANGED
@@ -342,10 +342,28 @@ 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();
349
367
  }
350
368
  /* ─── StorageAdapter v2 ───────────────────────────────────────────── */
351
369
  async load(_namespace) {
@@ -534,6 +552,7 @@ var CloudStorageAdapter = class {
534
552
  const fresh = this.readStoredToken();
535
553
  if (fresh) {
536
554
  this.cachedToken = fresh;
555
+ this.fireAuthChanged(fresh);
537
556
  return fresh;
538
557
  }
539
558
  if (!this.autoSignIn) {
@@ -542,6 +561,7 @@ var CloudStorageAdapter = class {
542
561
  if (!this.pendingAuth) {
543
562
  this.pendingAuth = this.openSignInPopup().then((t) => {
544
563
  this.writeStoredToken(t);
564
+ this.fireAuthChanged(t);
545
565
  return t;
546
566
  }).finally(() => {
547
567
  this.pendingAuth = null;
@@ -549,6 +569,22 @@ var CloudStorageAdapter = class {
549
569
  }
550
570
  return this.pendingAuth;
551
571
  }
572
+ /**
573
+ * Notify the host that the authenticated identity changed. De-duped
574
+ * via the token string so multiple resolveToken() hits with the same
575
+ * cached token don't re-call setUser on every fetch.
576
+ */
577
+ fireAuthChanged(token) {
578
+ if (this.lastNotifiedTokenId === token.token) return;
579
+ this.lastNotifiedTokenId = token.token;
580
+ try {
581
+ this.onAuthChanged?.({ userId: token.userId, user: token.user });
582
+ } catch (err) {
583
+ if (typeof console !== "undefined") {
584
+ console.warn("[vizu/cloud] onAuthChanged callback threw", err);
585
+ }
586
+ }
587
+ }
552
588
  openSignInPopup() {
553
589
  return new Promise((resolve, reject) => {
554
590
  if (typeof window === "undefined") {
@@ -581,7 +617,13 @@ var CloudStorageAdapter = class {
581
617
  if (typeof data.token !== "string" || typeof data.expiresAt !== "string" || typeof data.userId !== "string") return;
582
618
  resolved = true;
583
619
  cleanup();
584
- resolve({ token: data.token, expiresAt: data.expiresAt, userId: data.userId });
620
+ const userPayload = data.user;
621
+ const user = userPayload && typeof userPayload.name === "string" ? {
622
+ id: typeof userPayload.id === "string" ? userPayload.id : data.userId,
623
+ name: userPayload.name,
624
+ avatarUrl: typeof userPayload.avatarUrl === "string" ? userPayload.avatarUrl : void 0
625
+ } : null;
626
+ resolve({ token: data.token, expiresAt: data.expiresAt, userId: data.userId, user });
585
627
  };
586
628
  const onPoll = window.setInterval(() => {
587
629
  if (popup.closed) {
@@ -2676,7 +2718,7 @@ var DEFAULTS = {
2676
2718
  namespace: "default",
2677
2719
  accent: "#FF6647"
2678
2720
  };
2679
- function resolveStorage(storage, cloud, defaultMode) {
2721
+ function resolveStorage(storage, cloud, defaultMode, onAuthChanged) {
2680
2722
  if (cloud) {
2681
2723
  if (storage && typeof console !== "undefined") {
2682
2724
  console.warn("[vizu] Both `cloud` and `storage` set; `cloud` wins. Drop `storage` to silence this.");
@@ -2684,7 +2726,8 @@ function resolveStorage(storage, cloud, defaultMode) {
2684
2726
  return new CloudStorageAdapter({
2685
2727
  workspace: cloud.workspace,
2686
2728
  apiUrl: cloud.apiUrl,
2687
- autoSignIn: cloud.autoSignIn
2729
+ autoSignIn: cloud.autoSignIn,
2730
+ onAuthChanged
2688
2731
  });
2689
2732
  }
2690
2733
  if (!storage) return defaultMode === "local" ? new LocalStorageAdapter() : new InMemoryStorageAdapter();
@@ -2790,7 +2833,14 @@ var Vizu = class {
2790
2833
  this.opts.namespace = options.namespace ?? DEFAULTS.namespace;
2791
2834
  this.opts.accent = options.accent ?? DEFAULTS.accent;
2792
2835
  this.parsedShortcut = parseShortcut(this.opts.shortcut);
2793
- this.storage = resolveStorage(options.storage, options.cloud, _defaultStorage);
2836
+ this.storage = resolveStorage(
2837
+ options.storage,
2838
+ options.cloud,
2839
+ _defaultStorage,
2840
+ (info) => {
2841
+ if (info.user) this.setUser(info.user);
2842
+ }
2843
+ );
2794
2844
  this.user = options.user ?? null;
2795
2845
  if (options.actions) this.actions = [...options.actions];
2796
2846
  if (options.onCommentAdded) this.bus.on("comment:added", (p) => options.onCommentAdded(p.comment));
@@ -2836,6 +2886,14 @@ var Vizu = class {
2836
2886
  this.enabled = true;
2837
2887
  this.bus.emit("enabled", {});
2838
2888
  this.deferred(() => this.mount());
2889
+ if (this.storage instanceof CloudStorageAdapter) {
2890
+ void this.storage.preflightAuth().catch((err) => {
2891
+ if (err?.code === "auth_canceled" || err?.code === "popup_blocked") return;
2892
+ if (typeof console !== "undefined") {
2893
+ console.warn("[vizu] preflight auth failed:", err);
2894
+ }
2895
+ });
2896
+ }
2839
2897
  }
2840
2898
  disable() {
2841
2899
  if (!this.enabled) return;