@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/auto.js CHANGED
@@ -336,6 +336,19 @@ var CloudStorageAdapter = class {
336
336
  this.pendingAuth = null;
337
337
  /** Whether we've already fired onAuthChanged for the current token. */
338
338
  this.lastNotifiedTokenId = null;
339
+ /**
340
+ * Sticky flag set when /connect tells us this workspace is inaccessible
341
+ * to the signed-in user (no role, origin not allowed, workspace gone).
342
+ * Future resolveToken calls reject immediately instead of reopening the
343
+ * popup — the answer won't change without a Clerk account switch, which
344
+ * a page reload covers. Cleared only by reload (or future explicit API).
345
+ */
346
+ this.accessDenied = false;
347
+ /**
348
+ * Last denial reason from /connect's postMessage — useful for hosts that
349
+ * want to render a workspace-level "no access" banner. Null until denied.
350
+ */
351
+ this.accessDeniedReason = null;
339
352
  this.workspace = opts.workspace;
340
353
  this.apiUrl = (opts.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, "");
341
354
  this.autoSignIn = opts.autoSignIn !== false;
@@ -357,6 +370,20 @@ var CloudStorageAdapter = class {
357
370
  async preflightAuth() {
358
371
  await this.resolveToken();
359
372
  }
373
+ /**
374
+ * Whether /connect has already told us the user can't access this
375
+ * workspace. Vizu's write gate checks this before reopening the popup —
376
+ * a denied state means clicking again will yield the same "No access"
377
+ * page, so we suppress the retry until a page reload (or explicit API
378
+ * call in the future).
379
+ */
380
+ isAccessDenied() {
381
+ return this.accessDenied;
382
+ }
383
+ /** Last denial reason; null when not denied. */
384
+ accessDenialReason() {
385
+ return this.accessDeniedReason;
386
+ }
360
387
  /* ─── StorageAdapter v2 ───────────────────────────────────────────── */
361
388
  async load(_namespace) {
362
389
  const out = [];
@@ -547,6 +574,12 @@ var CloudStorageAdapter = class {
547
574
  this.fireAuthChanged(fresh);
548
575
  return fresh;
549
576
  }
577
+ if (this.accessDenied) {
578
+ throw makeAuthError(
579
+ "access_denied",
580
+ `No access to workspace "${this.workspace}" (${this.accessDeniedReason ?? "no_access"}).`
581
+ );
582
+ }
550
583
  if (!this.autoSignIn) {
551
584
  throw makeAuthError("auth_required", "Sign-in required; autoSignIn is disabled.");
552
585
  }
@@ -604,7 +637,21 @@ var CloudStorageAdapter = class {
604
637
  const onMessage = (e) => {
605
638
  if (e.origin !== apiOrigin) return;
606
639
  const data = e.data;
607
- if (!data || data.type !== "vizu:auth") return;
640
+ if (!data) return;
641
+ if (data.type === "vizu:auth-denied" && data.workspace === this.workspace) {
642
+ resolved = true;
643
+ cleanup();
644
+ this.accessDenied = true;
645
+ this.accessDeniedReason = typeof data.reason === "string" ? data.reason : "no_access";
646
+ reject(
647
+ makeAuthError(
648
+ "access_denied",
649
+ `No access to workspace "${this.workspace}" (${this.accessDeniedReason}).`
650
+ )
651
+ );
652
+ return;
653
+ }
654
+ if (data.type !== "vizu:auth") return;
608
655
  if (data.workspace !== this.workspace) return;
609
656
  if (typeof data.token !== "string" || typeof data.expiresAt !== "string" || typeof data.userId !== "string") return;
610
657
  resolved = true;
@@ -3143,6 +3190,24 @@ var Vizu = class {
3143
3190
  getUser() {
3144
3191
  return this.user;
3145
3192
  }
3193
+ /**
3194
+ * Cloud-mode write gate. Every entry point that creates or modifies a
3195
+ * comment first checks that we have a known user. Without one, the
3196
+ * write would land as Anonymous (or 401 on the backend) — neither is
3197
+ * the right UX. So we re-trigger the sign-in popup and refuse the
3198
+ * action. The user signs in, this.user gets set via onAuthChanged,
3199
+ * then they can retry the click themselves.
3200
+ *
3201
+ * Returns true when the caller may proceed.
3202
+ */
3203
+ requireUserOrAuth() {
3204
+ if (!(this.storage instanceof CloudStorageAdapter)) return true;
3205
+ if (this.user) return true;
3206
+ if (this.storage.isAccessDenied()) return false;
3207
+ void this.storage.preflightAuth().catch(() => {
3208
+ });
3209
+ return false;
3210
+ }
3146
3211
  /* ===== Lifecycle ===== */
3147
3212
  enable() {
3148
3213
  if (this.enabled) return;
@@ -3311,6 +3376,7 @@ var Vizu = class {
3311
3376
  * patch for adapters that don't implement reply ops.
3312
3377
  */
3313
3378
  async addReply(commentId, text, mentions) {
3379
+ if (!this.requireUserOrAuth()) return null;
3314
3380
  const idx = this.comments.findIndex((c) => c.id === commentId);
3315
3381
  if (idx === -1) return null;
3316
3382
  const reply = {
@@ -3377,6 +3443,7 @@ var Vizu = class {
3377
3443
  /* ===== Multi-select API ===== */
3378
3444
  /** Toggle multi-select mode. When on, plain clicks accumulate into the selection. */
3379
3445
  toggleMultiSelectMode() {
3446
+ if (!this.requireUserOrAuth()) return;
3380
3447
  this.multiSelectMode = !this.multiSelectMode;
3381
3448
  if (!this.multiSelectMode) this.clearSelection();
3382
3449
  this.pill?.setMultiSelectState(this.multiSelectMode, this.pendingFingerprints.length);
@@ -3538,6 +3605,7 @@ var Vizu = class {
3538
3605
  p.onSelect(fp2);
3539
3606
  return;
3540
3607
  }
3608
+ if (!this.requireUserOrAuth()) return;
3541
3609
  const fp = fingerprint(el);
3542
3610
  if (this.popover?.isOpen() && e.shiftKey) {
3543
3611
  this.popover.addAnchor(fp, el);
@@ -3585,6 +3653,7 @@ var Vizu = class {
3585
3653
  );
3586
3654
  }
3587
3655
  async addCommentFromPopover(text, fps, references, mentions, attachments) {
3656
+ if (!this.requireUserOrAuth()) return;
3588
3657
  if (fps.length === 0) return;
3589
3658
  const pageUrl = typeof location !== "undefined" ? location.href : void 0;
3590
3659
  const c = {