@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.d.cts
CHANGED
|
@@ -458,7 +458,29 @@ 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);
|
|
475
|
+
/**
|
|
476
|
+
* Pull a token out of the URL fragment if /connect just redirected us
|
|
477
|
+
* back here. Strips the fragment on success so the token doesn't sit
|
|
478
|
+
* in browser history or get shared if the user copies the URL.
|
|
479
|
+
*
|
|
480
|
+
* The fragment is base64-encoded JSON with compact keys; see
|
|
481
|
+
* apps/landing/app/connect/RedirectComplete.tsx for the producer side.
|
|
482
|
+
*/
|
|
483
|
+
private readHashToken;
|
|
462
484
|
/**
|
|
463
485
|
* Open the sign-in popup right now (or no-op if a valid token is
|
|
464
486
|
* cached). Vizu calls this on enable() so the user signs in BEFORE
|
|
@@ -469,6 +491,16 @@ declare class CloudStorageAdapter implements StorageAdapter {
|
|
|
469
491
|
* state.
|
|
470
492
|
*/
|
|
471
493
|
preflightAuth(): Promise<void>;
|
|
494
|
+
/**
|
|
495
|
+
* Whether /connect has already told us the user can't access this
|
|
496
|
+
* workspace. Vizu's write gate checks this before reopening the popup —
|
|
497
|
+
* a denied state means clicking again will yield the same "No access"
|
|
498
|
+
* page, so we suppress the retry until a page reload (or explicit API
|
|
499
|
+
* call in the future).
|
|
500
|
+
*/
|
|
501
|
+
isAccessDenied(): boolean;
|
|
502
|
+
/** Last denial reason; null when not denied. */
|
|
503
|
+
accessDenialReason(): string | null;
|
|
472
504
|
load(_namespace: string): Promise<VizuComment[]>;
|
|
473
505
|
addComment(_namespace: string, comment: VizuComment): Promise<void>;
|
|
474
506
|
updateComment(_namespace: string, id: string, patch: Partial<VizuComment>): Promise<VizuComment | null>;
|
|
@@ -521,7 +553,9 @@ declare class CloudStorageAdapter implements StorageAdapter {
|
|
|
521
553
|
private fetchAuthed;
|
|
522
554
|
private parseJson;
|
|
523
555
|
}
|
|
524
|
-
type CloudErrorCode = 'auth_required' | 'auth_canceled' | 'auth_unavailable' | 'popup_blocked' | 'origin_not_allowed' | 'workspace_not_found'
|
|
556
|
+
type CloudErrorCode = 'auth_required' | 'auth_canceled' | 'auth_unavailable' | 'popup_blocked' | 'origin_not_allowed' | 'workspace_not_found'
|
|
557
|
+
/** /connect resolved access for the signed-in user → no role on the workspace. Sticky for the session. */
|
|
558
|
+
| 'access_denied' | 'http_error';
|
|
525
559
|
interface CloudError extends Error {
|
|
526
560
|
code: CloudErrorCode;
|
|
527
561
|
status?: number;
|
package/dist/index.d.ts
CHANGED
|
@@ -458,7 +458,29 @@ 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);
|
|
475
|
+
/**
|
|
476
|
+
* Pull a token out of the URL fragment if /connect just redirected us
|
|
477
|
+
* back here. Strips the fragment on success so the token doesn't sit
|
|
478
|
+
* in browser history or get shared if the user copies the URL.
|
|
479
|
+
*
|
|
480
|
+
* The fragment is base64-encoded JSON with compact keys; see
|
|
481
|
+
* apps/landing/app/connect/RedirectComplete.tsx for the producer side.
|
|
482
|
+
*/
|
|
483
|
+
private readHashToken;
|
|
462
484
|
/**
|
|
463
485
|
* Open the sign-in popup right now (or no-op if a valid token is
|
|
464
486
|
* cached). Vizu calls this on enable() so the user signs in BEFORE
|
|
@@ -469,6 +491,16 @@ declare class CloudStorageAdapter implements StorageAdapter {
|
|
|
469
491
|
* state.
|
|
470
492
|
*/
|
|
471
493
|
preflightAuth(): Promise<void>;
|
|
494
|
+
/**
|
|
495
|
+
* Whether /connect has already told us the user can't access this
|
|
496
|
+
* workspace. Vizu's write gate checks this before reopening the popup —
|
|
497
|
+
* a denied state means clicking again will yield the same "No access"
|
|
498
|
+
* page, so we suppress the retry until a page reload (or explicit API
|
|
499
|
+
* call in the future).
|
|
500
|
+
*/
|
|
501
|
+
isAccessDenied(): boolean;
|
|
502
|
+
/** Last denial reason; null when not denied. */
|
|
503
|
+
accessDenialReason(): string | null;
|
|
472
504
|
load(_namespace: string): Promise<VizuComment[]>;
|
|
473
505
|
addComment(_namespace: string, comment: VizuComment): Promise<void>;
|
|
474
506
|
updateComment(_namespace: string, id: string, patch: Partial<VizuComment>): Promise<VizuComment | null>;
|
|
@@ -521,7 +553,9 @@ declare class CloudStorageAdapter implements StorageAdapter {
|
|
|
521
553
|
private fetchAuthed;
|
|
522
554
|
private parseJson;
|
|
523
555
|
}
|
|
524
|
-
type CloudErrorCode = 'auth_required' | 'auth_canceled' | 'auth_unavailable' | 'popup_blocked' | 'origin_not_allowed' | 'workspace_not_found'
|
|
556
|
+
type CloudErrorCode = 'auth_required' | 'auth_canceled' | 'auth_unavailable' | 'popup_blocked' | 'origin_not_allowed' | 'workspace_not_found'
|
|
557
|
+
/** /connect resolved access for the signed-in user → no role on the workspace. Sticky for the session. */
|
|
558
|
+
| 'access_denied' | 'http_error';
|
|
525
559
|
interface CloudError extends Error {
|
|
526
560
|
code: CloudErrorCode;
|
|
527
561
|
status?: number;
|
package/dist/index.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
|
-
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
|
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;
|