@unhingged/vizu-core 0.1.4 → 0.1.6

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,15 +336,76 @@ 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;
342
355
  this.onAuthChanged = opts.onAuthChanged;
343
- this.cachedToken = this.readStoredToken();
356
+ const fromHash = this.readHashToken();
357
+ if (fromHash) {
358
+ this.cachedToken = fromHash;
359
+ this.writeStoredToken(fromHash);
360
+ } else {
361
+ this.cachedToken = this.readStoredToken();
362
+ }
344
363
  if (this.cachedToken && !this.isExpired(this.cachedToken)) {
345
364
  this.fireAuthChanged(this.cachedToken);
346
365
  }
347
366
  }
367
+ /**
368
+ * Pull a token out of the URL fragment if /connect just redirected us
369
+ * back here. Strips the fragment on success so the token doesn't sit
370
+ * in browser history or get shared if the user copies the URL.
371
+ *
372
+ * The fragment is base64-encoded JSON with compact keys; see
373
+ * apps/landing/app/connect/RedirectComplete.tsx for the producer side.
374
+ */
375
+ readHashToken() {
376
+ if (typeof window === "undefined") return null;
377
+ const hash = window.location.hash;
378
+ if (!hash || hash.length < 2) return null;
379
+ const params = new URLSearchParams(hash.slice(1));
380
+ const payload = params.get("vizu_auth");
381
+ if (!payload) return null;
382
+ try {
383
+ const json = JSON.parse(atob(payload));
384
+ if (!json.t || !json.e || !json.u || !json.w) return null;
385
+ if (json.w !== this.workspace) return null;
386
+ const stored = {
387
+ token: json.t,
388
+ expiresAt: json.e,
389
+ userId: json.u,
390
+ user: typeof json.n === "string" ? {
391
+ id: json.u,
392
+ name: json.n,
393
+ avatarUrl: typeof json.a === "string" ? json.a : void 0
394
+ } : null
395
+ };
396
+ params.delete("vizu_auth");
397
+ const remaining = params.toString();
398
+ const newHash = remaining ? `#${remaining}` : "";
399
+ try {
400
+ const newUrl = window.location.pathname + window.location.search + newHash;
401
+ window.history.replaceState(null, "", newUrl);
402
+ } catch {
403
+ }
404
+ return stored;
405
+ } catch {
406
+ return null;
407
+ }
408
+ }
348
409
  /**
349
410
  * Open the sign-in popup right now (or no-op if a valid token is
350
411
  * cached). Vizu calls this on enable() so the user signs in BEFORE
@@ -357,6 +418,20 @@ var CloudStorageAdapter = class {
357
418
  async preflightAuth() {
358
419
  await this.resolveToken();
359
420
  }
421
+ /**
422
+ * Whether /connect has already told us the user can't access this
423
+ * workspace. Vizu's write gate checks this before reopening the popup —
424
+ * a denied state means clicking again will yield the same "No access"
425
+ * page, so we suppress the retry until a page reload (or explicit API
426
+ * call in the future).
427
+ */
428
+ isAccessDenied() {
429
+ return this.accessDenied;
430
+ }
431
+ /** Last denial reason; null when not denied. */
432
+ accessDenialReason() {
433
+ return this.accessDeniedReason;
434
+ }
360
435
  /* ─── StorageAdapter v2 ───────────────────────────────────────────── */
361
436
  async load(_namespace) {
362
437
  const out = [];
@@ -547,6 +622,12 @@ var CloudStorageAdapter = class {
547
622
  this.fireAuthChanged(fresh);
548
623
  return fresh;
549
624
  }
625
+ if (this.accessDenied) {
626
+ throw makeAuthError(
627
+ "access_denied",
628
+ `No access to workspace "${this.workspace}" (${this.accessDeniedReason ?? "no_access"}).`
629
+ );
630
+ }
550
631
  if (!this.autoSignIn) {
551
632
  throw makeAuthError("auth_required", "Sign-in required; autoSignIn is disabled.");
552
633
  }
@@ -593,18 +674,39 @@ var CloudStorageAdapter = class {
593
674
  `width=${POPUP_WIDTH},height=${POPUP_HEIGHT},left=${left},top=${top},noopener=no,popup=yes`
594
675
  );
595
676
  if (!popup) {
596
- return reject(
597
- makeAuthError(
598
- "popup_blocked",
599
- "Sign-in popup was blocked by the browser. Allow popups for this site and try again."
600
- )
601
- );
677
+ try {
678
+ const returnTo = window.location.href;
679
+ const redirectUrl = this.apiUrl + "/connect?workspace=" + encodeURIComponent(this.workspace) + "&origin=" + encodeURIComponent(hostOrigin) + "&return_to=" + encodeURIComponent(returnTo);
680
+ window.location.assign(redirectUrl);
681
+ return;
682
+ } catch (err) {
683
+ return reject(
684
+ makeAuthError(
685
+ "popup_blocked",
686
+ "Sign-in popup was blocked and redirect fallback failed: " + (err instanceof Error ? err.message : String(err))
687
+ )
688
+ );
689
+ }
602
690
  }
603
691
  let resolved = false;
604
692
  const onMessage = (e) => {
605
693
  if (e.origin !== apiOrigin) return;
606
694
  const data = e.data;
607
- if (!data || data.type !== "vizu:auth") return;
695
+ if (!data) return;
696
+ if (data.type === "vizu:auth-denied" && data.workspace === this.workspace) {
697
+ resolved = true;
698
+ cleanup();
699
+ this.accessDenied = true;
700
+ this.accessDeniedReason = typeof data.reason === "string" ? data.reason : "no_access";
701
+ reject(
702
+ makeAuthError(
703
+ "access_denied",
704
+ `No access to workspace "${this.workspace}" (${this.accessDeniedReason}).`
705
+ )
706
+ );
707
+ return;
708
+ }
709
+ if (data.type !== "vizu:auth") return;
608
710
  if (data.workspace !== this.workspace) return;
609
711
  if (typeof data.token !== "string" || typeof data.expiresAt !== "string" || typeof data.userId !== "string") return;
610
712
  resolved = true;
@@ -3156,6 +3258,7 @@ var Vizu = class {
3156
3258
  requireUserOrAuth() {
3157
3259
  if (!(this.storage instanceof CloudStorageAdapter)) return true;
3158
3260
  if (this.user) return true;
3261
+ if (this.storage.isAccessDenied()) return false;
3159
3262
  void this.storage.preflightAuth().catch(() => {
3160
3263
  });
3161
3264
  return false;