@unhingged/vizu-core 0.1.4 → 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.d.cts CHANGED
@@ -458,6 +458,19 @@ declare class CloudStorageAdapter implements StorageAdapter {
458
458
  private pendingAuth;
459
459
  /** Whether we've already fired onAuthChanged for the current token. */
460
460
  private lastNotifiedTokenId;
461
+ /**
462
+ * Sticky flag set when /connect tells us this workspace is inaccessible
463
+ * to the signed-in user (no role, origin not allowed, workspace gone).
464
+ * Future resolveToken calls reject immediately instead of reopening the
465
+ * popup — the answer won't change without a Clerk account switch, which
466
+ * a page reload covers. Cleared only by reload (or future explicit API).
467
+ */
468
+ private accessDenied;
469
+ /**
470
+ * Last denial reason from /connect's postMessage — useful for hosts that
471
+ * want to render a workspace-level "no access" banner. Null until denied.
472
+ */
473
+ private accessDeniedReason;
461
474
  constructor(opts: CloudStorageOptions);
462
475
  /**
463
476
  * Open the sign-in popup right now (or no-op if a valid token is
@@ -469,6 +482,16 @@ declare class CloudStorageAdapter implements StorageAdapter {
469
482
  * state.
470
483
  */
471
484
  preflightAuth(): Promise<void>;
485
+ /**
486
+ * Whether /connect has already told us the user can't access this
487
+ * workspace. Vizu's write gate checks this before reopening the popup —
488
+ * a denied state means clicking again will yield the same "No access"
489
+ * page, so we suppress the retry until a page reload (or explicit API
490
+ * call in the future).
491
+ */
492
+ isAccessDenied(): boolean;
493
+ /** Last denial reason; null when not denied. */
494
+ accessDenialReason(): string | null;
472
495
  load(_namespace: string): Promise<VizuComment[]>;
473
496
  addComment(_namespace: string, comment: VizuComment): Promise<void>;
474
497
  updateComment(_namespace: string, id: string, patch: Partial<VizuComment>): Promise<VizuComment | null>;
@@ -521,7 +544,9 @@ declare class CloudStorageAdapter implements StorageAdapter {
521
544
  private fetchAuthed;
522
545
  private parseJson;
523
546
  }
524
- type CloudErrorCode = 'auth_required' | 'auth_canceled' | 'auth_unavailable' | 'popup_blocked' | 'origin_not_allowed' | 'workspace_not_found' | 'http_error';
547
+ type CloudErrorCode = 'auth_required' | 'auth_canceled' | 'auth_unavailable' | 'popup_blocked' | 'origin_not_allowed' | 'workspace_not_found'
548
+ /** /connect resolved access for the signed-in user → no role on the workspace. Sticky for the session. */
549
+ | 'access_denied' | 'http_error';
525
550
  interface CloudError extends Error {
526
551
  code: CloudErrorCode;
527
552
  status?: number;
package/dist/index.d.ts CHANGED
@@ -458,6 +458,19 @@ declare class CloudStorageAdapter implements StorageAdapter {
458
458
  private pendingAuth;
459
459
  /** Whether we've already fired onAuthChanged for the current token. */
460
460
  private lastNotifiedTokenId;
461
+ /**
462
+ * Sticky flag set when /connect tells us this workspace is inaccessible
463
+ * to the signed-in user (no role, origin not allowed, workspace gone).
464
+ * Future resolveToken calls reject immediately instead of reopening the
465
+ * popup — the answer won't change without a Clerk account switch, which
466
+ * a page reload covers. Cleared only by reload (or future explicit API).
467
+ */
468
+ private accessDenied;
469
+ /**
470
+ * Last denial reason from /connect's postMessage — useful for hosts that
471
+ * want to render a workspace-level "no access" banner. Null until denied.
472
+ */
473
+ private accessDeniedReason;
461
474
  constructor(opts: CloudStorageOptions);
462
475
  /**
463
476
  * Open the sign-in popup right now (or no-op if a valid token is
@@ -469,6 +482,16 @@ declare class CloudStorageAdapter implements StorageAdapter {
469
482
  * state.
470
483
  */
471
484
  preflightAuth(): Promise<void>;
485
+ /**
486
+ * Whether /connect has already told us the user can't access this
487
+ * workspace. Vizu's write gate checks this before reopening the popup —
488
+ * a denied state means clicking again will yield the same "No access"
489
+ * page, so we suppress the retry until a page reload (or explicit API
490
+ * call in the future).
491
+ */
492
+ isAccessDenied(): boolean;
493
+ /** Last denial reason; null when not denied. */
494
+ accessDenialReason(): string | null;
472
495
  load(_namespace: string): Promise<VizuComment[]>;
473
496
  addComment(_namespace: string, comment: VizuComment): Promise<void>;
474
497
  updateComment(_namespace: string, id: string, patch: Partial<VizuComment>): Promise<VizuComment | null>;
@@ -521,7 +544,9 @@ declare class CloudStorageAdapter implements StorageAdapter {
521
544
  private fetchAuthed;
522
545
  private parseJson;
523
546
  }
524
- type CloudErrorCode = 'auth_required' | 'auth_canceled' | 'auth_unavailable' | 'popup_blocked' | 'origin_not_allowed' | 'workspace_not_found' | 'http_error';
547
+ type CloudErrorCode = 'auth_required' | 'auth_canceled' | 'auth_unavailable' | 'popup_blocked' | 'origin_not_allowed' | 'workspace_not_found'
548
+ /** /connect resolved access for the signed-in user → no role on the workspace. Sticky for the session. */
549
+ | 'access_denied' | 'http_error';
525
550
  interface CloudError extends Error {
526
551
  code: CloudErrorCode;
527
552
  status?: number;
package/dist/index.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;
@@ -3156,6 +3203,7 @@ var Vizu = class {
3156
3203
  requireUserOrAuth() {
3157
3204
  if (!(this.storage instanceof CloudStorageAdapter)) return true;
3158
3205
  if (this.user) return true;
3206
+ if (this.storage.isAccessDenied()) return false;
3159
3207
  void this.storage.preflightAuth().catch(() => {
3160
3208
  });
3161
3209
  return false;