@unhingged/vizu-core 0.1.3 → 0.1.5

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.cjs CHANGED
@@ -372,6 +372,19 @@ var CloudStorageAdapter = class {
372
372
  this.pendingAuth = null;
373
373
  /** Whether we've already fired onAuthChanged for the current token. */
374
374
  this.lastNotifiedTokenId = null;
375
+ /**
376
+ * Sticky flag set when /connect tells us this workspace is inaccessible
377
+ * to the signed-in user (no role, origin not allowed, workspace gone).
378
+ * Future resolveToken calls reject immediately instead of reopening the
379
+ * popup — the answer won't change without a Clerk account switch, which
380
+ * a page reload covers. Cleared only by reload (or future explicit API).
381
+ */
382
+ this.accessDenied = false;
383
+ /**
384
+ * Last denial reason from /connect's postMessage — useful for hosts that
385
+ * want to render a workspace-level "no access" banner. Null until denied.
386
+ */
387
+ this.accessDeniedReason = null;
375
388
  this.workspace = opts.workspace;
376
389
  this.apiUrl = (opts.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, "");
377
390
  this.autoSignIn = opts.autoSignIn !== false;
@@ -393,6 +406,20 @@ var CloudStorageAdapter = class {
393
406
  async preflightAuth() {
394
407
  await this.resolveToken();
395
408
  }
409
+ /**
410
+ * Whether /connect has already told us the user can't access this
411
+ * workspace. Vizu's write gate checks this before reopening the popup —
412
+ * a denied state means clicking again will yield the same "No access"
413
+ * page, so we suppress the retry until a page reload (or explicit API
414
+ * call in the future).
415
+ */
416
+ isAccessDenied() {
417
+ return this.accessDenied;
418
+ }
419
+ /** Last denial reason; null when not denied. */
420
+ accessDenialReason() {
421
+ return this.accessDeniedReason;
422
+ }
396
423
  /* ─── StorageAdapter v2 ───────────────────────────────────────────── */
397
424
  async load(_namespace) {
398
425
  const out = [];
@@ -583,6 +610,12 @@ var CloudStorageAdapter = class {
583
610
  this.fireAuthChanged(fresh);
584
611
  return fresh;
585
612
  }
613
+ if (this.accessDenied) {
614
+ throw makeAuthError(
615
+ "access_denied",
616
+ `No access to workspace "${this.workspace}" (${this.accessDeniedReason ?? "no_access"}).`
617
+ );
618
+ }
586
619
  if (!this.autoSignIn) {
587
620
  throw makeAuthError("auth_required", "Sign-in required; autoSignIn is disabled.");
588
621
  }
@@ -640,7 +673,21 @@ var CloudStorageAdapter = class {
640
673
  const onMessage = (e) => {
641
674
  if (e.origin !== apiOrigin) return;
642
675
  const data = e.data;
643
- if (!data || data.type !== "vizu:auth") return;
676
+ if (!data) return;
677
+ if (data.type === "vizu:auth-denied" && data.workspace === this.workspace) {
678
+ resolved = true;
679
+ cleanup();
680
+ this.accessDenied = true;
681
+ this.accessDeniedReason = typeof data.reason === "string" ? data.reason : "no_access";
682
+ reject(
683
+ makeAuthError(
684
+ "access_denied",
685
+ `No access to workspace "${this.workspace}" (${this.accessDeniedReason}).`
686
+ )
687
+ );
688
+ return;
689
+ }
690
+ if (data.type !== "vizu:auth") return;
644
691
  if (data.workspace !== this.workspace) return;
645
692
  if (typeof data.token !== "string" || typeof data.expiresAt !== "string" || typeof data.userId !== "string") return;
646
693
  resolved = true;
@@ -3179,6 +3226,24 @@ var Vizu = class {
3179
3226
  getUser() {
3180
3227
  return this.user;
3181
3228
  }
3229
+ /**
3230
+ * Cloud-mode write gate. Every entry point that creates or modifies a
3231
+ * comment first checks that we have a known user. Without one, the
3232
+ * write would land as Anonymous (or 401 on the backend) — neither is
3233
+ * the right UX. So we re-trigger the sign-in popup and refuse the
3234
+ * action. The user signs in, this.user gets set via onAuthChanged,
3235
+ * then they can retry the click themselves.
3236
+ *
3237
+ * Returns true when the caller may proceed.
3238
+ */
3239
+ requireUserOrAuth() {
3240
+ if (!(this.storage instanceof CloudStorageAdapter)) return true;
3241
+ if (this.user) return true;
3242
+ if (this.storage.isAccessDenied()) return false;
3243
+ void this.storage.preflightAuth().catch(() => {
3244
+ });
3245
+ return false;
3246
+ }
3182
3247
  /* ===== Lifecycle ===== */
3183
3248
  enable() {
3184
3249
  if (this.enabled) return;
@@ -3347,6 +3412,7 @@ var Vizu = class {
3347
3412
  * patch for adapters that don't implement reply ops.
3348
3413
  */
3349
3414
  async addReply(commentId, text, mentions) {
3415
+ if (!this.requireUserOrAuth()) return null;
3350
3416
  const idx = this.comments.findIndex((c) => c.id === commentId);
3351
3417
  if (idx === -1) return null;
3352
3418
  const reply = {
@@ -3413,6 +3479,7 @@ var Vizu = class {
3413
3479
  /* ===== Multi-select API ===== */
3414
3480
  /** Toggle multi-select mode. When on, plain clicks accumulate into the selection. */
3415
3481
  toggleMultiSelectMode() {
3482
+ if (!this.requireUserOrAuth()) return;
3416
3483
  this.multiSelectMode = !this.multiSelectMode;
3417
3484
  if (!this.multiSelectMode) this.clearSelection();
3418
3485
  this.pill?.setMultiSelectState(this.multiSelectMode, this.pendingFingerprints.length);
@@ -3574,6 +3641,7 @@ var Vizu = class {
3574
3641
  p.onSelect(fp2);
3575
3642
  return;
3576
3643
  }
3644
+ if (!this.requireUserOrAuth()) return;
3577
3645
  const fp = fingerprint(el);
3578
3646
  if (this.popover?.isOpen() && e.shiftKey) {
3579
3647
  this.popover.addAnchor(fp, el);
@@ -3621,6 +3689,7 @@ var Vizu = class {
3621
3689
  );
3622
3690
  }
3623
3691
  async addCommentFromPopover(text, fps, references, mentions, attachments) {
3692
+ if (!this.requireUserOrAuth()) return;
3624
3693
  if (fps.length === 0) return;
3625
3694
  const pageUrl = typeof location !== "undefined" ? location.href : void 0;
3626
3695
  const c = {