@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.cjs +111 -8
- package/dist/auto.cjs.map +1 -1
- package/dist/auto.js +111 -8
- package/dist/auto.js.map +1 -1
- package/dist/index.cjs +111 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -1
- package/dist/index.d.ts +35 -1
- package/dist/index.js +111 -8
- package/dist/index.js.map +1 -1
- package/dist/vizu.min.js +37 -37
- package/dist/vizu.min.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -372,15 +372,76 @@ 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;
|
|
378
391
|
this.onAuthChanged = opts.onAuthChanged;
|
|
379
|
-
|
|
392
|
+
const fromHash = this.readHashToken();
|
|
393
|
+
if (fromHash) {
|
|
394
|
+
this.cachedToken = fromHash;
|
|
395
|
+
this.writeStoredToken(fromHash);
|
|
396
|
+
} else {
|
|
397
|
+
this.cachedToken = this.readStoredToken();
|
|
398
|
+
}
|
|
380
399
|
if (this.cachedToken && !this.isExpired(this.cachedToken)) {
|
|
381
400
|
this.fireAuthChanged(this.cachedToken);
|
|
382
401
|
}
|
|
383
402
|
}
|
|
403
|
+
/**
|
|
404
|
+
* Pull a token out of the URL fragment if /connect just redirected us
|
|
405
|
+
* back here. Strips the fragment on success so the token doesn't sit
|
|
406
|
+
* in browser history or get shared if the user copies the URL.
|
|
407
|
+
*
|
|
408
|
+
* The fragment is base64-encoded JSON with compact keys; see
|
|
409
|
+
* apps/landing/app/connect/RedirectComplete.tsx for the producer side.
|
|
410
|
+
*/
|
|
411
|
+
readHashToken() {
|
|
412
|
+
if (typeof window === "undefined") return null;
|
|
413
|
+
const hash = window.location.hash;
|
|
414
|
+
if (!hash || hash.length < 2) return null;
|
|
415
|
+
const params = new URLSearchParams(hash.slice(1));
|
|
416
|
+
const payload = params.get("vizu_auth");
|
|
417
|
+
if (!payload) return null;
|
|
418
|
+
try {
|
|
419
|
+
const json = JSON.parse(atob(payload));
|
|
420
|
+
if (!json.t || !json.e || !json.u || !json.w) return null;
|
|
421
|
+
if (json.w !== this.workspace) return null;
|
|
422
|
+
const stored = {
|
|
423
|
+
token: json.t,
|
|
424
|
+
expiresAt: json.e,
|
|
425
|
+
userId: json.u,
|
|
426
|
+
user: typeof json.n === "string" ? {
|
|
427
|
+
id: json.u,
|
|
428
|
+
name: json.n,
|
|
429
|
+
avatarUrl: typeof json.a === "string" ? json.a : void 0
|
|
430
|
+
} : null
|
|
431
|
+
};
|
|
432
|
+
params.delete("vizu_auth");
|
|
433
|
+
const remaining = params.toString();
|
|
434
|
+
const newHash = remaining ? `#${remaining}` : "";
|
|
435
|
+
try {
|
|
436
|
+
const newUrl = window.location.pathname + window.location.search + newHash;
|
|
437
|
+
window.history.replaceState(null, "", newUrl);
|
|
438
|
+
} catch {
|
|
439
|
+
}
|
|
440
|
+
return stored;
|
|
441
|
+
} catch {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
384
445
|
/**
|
|
385
446
|
* Open the sign-in popup right now (or no-op if a valid token is
|
|
386
447
|
* cached). Vizu calls this on enable() so the user signs in BEFORE
|
|
@@ -393,6 +454,20 @@ var CloudStorageAdapter = class {
|
|
|
393
454
|
async preflightAuth() {
|
|
394
455
|
await this.resolveToken();
|
|
395
456
|
}
|
|
457
|
+
/**
|
|
458
|
+
* Whether /connect has already told us the user can't access this
|
|
459
|
+
* workspace. Vizu's write gate checks this before reopening the popup —
|
|
460
|
+
* a denied state means clicking again will yield the same "No access"
|
|
461
|
+
* page, so we suppress the retry until a page reload (or explicit API
|
|
462
|
+
* call in the future).
|
|
463
|
+
*/
|
|
464
|
+
isAccessDenied() {
|
|
465
|
+
return this.accessDenied;
|
|
466
|
+
}
|
|
467
|
+
/** Last denial reason; null when not denied. */
|
|
468
|
+
accessDenialReason() {
|
|
469
|
+
return this.accessDeniedReason;
|
|
470
|
+
}
|
|
396
471
|
/* ─── StorageAdapter v2 ───────────────────────────────────────────── */
|
|
397
472
|
async load(_namespace) {
|
|
398
473
|
const out = [];
|
|
@@ -583,6 +658,12 @@ var CloudStorageAdapter = class {
|
|
|
583
658
|
this.fireAuthChanged(fresh);
|
|
584
659
|
return fresh;
|
|
585
660
|
}
|
|
661
|
+
if (this.accessDenied) {
|
|
662
|
+
throw makeAuthError(
|
|
663
|
+
"access_denied",
|
|
664
|
+
`No access to workspace "${this.workspace}" (${this.accessDeniedReason ?? "no_access"}).`
|
|
665
|
+
);
|
|
666
|
+
}
|
|
586
667
|
if (!this.autoSignIn) {
|
|
587
668
|
throw makeAuthError("auth_required", "Sign-in required; autoSignIn is disabled.");
|
|
588
669
|
}
|
|
@@ -629,18 +710,39 @@ var CloudStorageAdapter = class {
|
|
|
629
710
|
`width=${POPUP_WIDTH},height=${POPUP_HEIGHT},left=${left},top=${top},noopener=no,popup=yes`
|
|
630
711
|
);
|
|
631
712
|
if (!popup) {
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
)
|
|
713
|
+
try {
|
|
714
|
+
const returnTo = window.location.href;
|
|
715
|
+
const redirectUrl = this.apiUrl + "/connect?workspace=" + encodeURIComponent(this.workspace) + "&origin=" + encodeURIComponent(hostOrigin) + "&return_to=" + encodeURIComponent(returnTo);
|
|
716
|
+
window.location.assign(redirectUrl);
|
|
717
|
+
return;
|
|
718
|
+
} catch (err) {
|
|
719
|
+
return reject(
|
|
720
|
+
makeAuthError(
|
|
721
|
+
"popup_blocked",
|
|
722
|
+
"Sign-in popup was blocked and redirect fallback failed: " + (err instanceof Error ? err.message : String(err))
|
|
723
|
+
)
|
|
724
|
+
);
|
|
725
|
+
}
|
|
638
726
|
}
|
|
639
727
|
let resolved = false;
|
|
640
728
|
const onMessage = (e) => {
|
|
641
729
|
if (e.origin !== apiOrigin) return;
|
|
642
730
|
const data = e.data;
|
|
643
|
-
if (!data
|
|
731
|
+
if (!data) return;
|
|
732
|
+
if (data.type === "vizu:auth-denied" && data.workspace === this.workspace) {
|
|
733
|
+
resolved = true;
|
|
734
|
+
cleanup();
|
|
735
|
+
this.accessDenied = true;
|
|
736
|
+
this.accessDeniedReason = typeof data.reason === "string" ? data.reason : "no_access";
|
|
737
|
+
reject(
|
|
738
|
+
makeAuthError(
|
|
739
|
+
"access_denied",
|
|
740
|
+
`No access to workspace "${this.workspace}" (${this.accessDeniedReason}).`
|
|
741
|
+
)
|
|
742
|
+
);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
if (data.type !== "vizu:auth") return;
|
|
644
746
|
if (data.workspace !== this.workspace) return;
|
|
645
747
|
if (typeof data.token !== "string" || typeof data.expiresAt !== "string" || typeof data.userId !== "string") return;
|
|
646
748
|
resolved = true;
|
|
@@ -3192,6 +3294,7 @@ var Vizu = class {
|
|
|
3192
3294
|
requireUserOrAuth() {
|
|
3193
3295
|
if (!(this.storage instanceof CloudStorageAdapter)) return true;
|
|
3194
3296
|
if (this.user) return true;
|
|
3297
|
+
if (this.storage.isAccessDenied()) return false;
|
|
3195
3298
|
void this.storage.preflightAuth().catch(() => {
|
|
3196
3299
|
});
|
|
3197
3300
|
return false;
|