Forked from hermes-frontend, stripped openclaw/bun specifics: - Auth tokens: openclaw_session -> nyx_session - Vite proxy: localhost:3003 -> localhost:8000 (assay) - Prod WS: wss://assay.loop42.de/ws - Workspace paths: removed openclaw-specific paths - Added missing deps: @heroicons/vue, overlayscrollbars-vue - Branding: title -> nyx Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
119 lines
3.8 KiB
TypeScript
119 lines
3.8 KiB
TypeScript
/**
|
|
* store/viewer.ts — Viewer singleton state (fstoken + roots)
|
|
*
|
|
* fstoken has a 1h TTL on the backend. We cache it in sessionStorage so
|
|
* F5 and agent→viewer transitions are instant (stale-while-revalidate).
|
|
*
|
|
* acquire() is idempotent: no-op if token is fresh, re-acquires if within
|
|
* REFRESH_BEFORE_EXPIRY of expiry. Safe to call from multiple places.
|
|
*/
|
|
|
|
import { defineStore } from 'pinia';
|
|
import { ref, computed } from 'vue';
|
|
import { getApiBase } from '../utils/apiBase';
|
|
|
|
const STORAGE_KEY = 'viewer_auth';
|
|
const REFRESH_BEFORE_EXPIRY_MS = 5 * 60 * 1000; // refresh if <5min left
|
|
|
|
interface ViewerAuth {
|
|
fstoken: string;
|
|
roots: string[];
|
|
expiresAt: number; // ms epoch, client-side estimate (TTL - 1h)
|
|
}
|
|
|
|
|
|
|
|
function loadStored(): ViewerAuth | null {
|
|
try {
|
|
const raw = sessionStorage.getItem(STORAGE_KEY);
|
|
if (!raw) return null;
|
|
return JSON.parse(raw) as ViewerAuth;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function saveStored(auth: ViewerAuth) {
|
|
try {
|
|
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(auth));
|
|
} catch {}
|
|
}
|
|
|
|
function clearStored() {
|
|
sessionStorage.removeItem(STORAGE_KEY);
|
|
}
|
|
|
|
export const useViewerStore = defineStore('viewer', () => {
|
|
const stored = loadStored();
|
|
|
|
const fstoken = ref<string>(stored?.fstoken ?? '');
|
|
const roots = ref<string[]>(stored?.roots ?? []);
|
|
const expiresAt = ref<number>(stored?.expiresAt ?? 0);
|
|
|
|
// true once we have a valid (non-expired) token
|
|
const ready = computed(() => !!fstoken.value && Date.now() < expiresAt.value);
|
|
|
|
let acquiring: Promise<void> | null = null;
|
|
|
|
async function acquire(force = false): Promise<void> {
|
|
// Skip if token is fresh (more than REFRESH_BEFORE_EXPIRY_MS left)
|
|
if (!force && fstoken.value && Date.now() < expiresAt.value - REFRESH_BEFORE_EXPIRY_MS) return;
|
|
|
|
// Deduplicate concurrent calls
|
|
if (acquiring) return acquiring;
|
|
|
|
acquiring = _acquire().finally(() => { acquiring = null; });
|
|
return acquiring;
|
|
}
|
|
|
|
async function _acquire(): Promise<void> {
|
|
const sessionToken = localStorage.getItem('nyx_session') || localStorage.getItem('titan_token') || '';
|
|
if (!sessionToken) return;
|
|
|
|
try {
|
|
const res = await fetch(`${getApiBase()}/api/viewer/token`, {
|
|
method: 'POST',
|
|
headers: { Authorization: `Bearer ${sessionToken}` },
|
|
});
|
|
if (!res.ok) {
|
|
// token rejected — clear stale state
|
|
fstoken.value = '';
|
|
roots.value = [];
|
|
expiresAt.value = 0;
|
|
clearStored();
|
|
return;
|
|
}
|
|
const data = await res.json();
|
|
const newToken: string = data.fstoken;
|
|
|
|
// Fetch roots while we have the token
|
|
let newRoots: string[] = roots.value.length ? roots.value : ['shared', 'workspace-titan'];
|
|
try {
|
|
const tr = await fetch(`${getApiBase()}/api/viewer/tree?root=&token=${encodeURIComponent(newToken)}`);
|
|
if (tr.ok) {
|
|
const td = await tr.json();
|
|
if (Array.isArray(td.dirs) && td.dirs.length) newRoots = td.dirs;
|
|
}
|
|
} catch {}
|
|
|
|
// Client-side expiry estimate: backend TTL is 1h
|
|
const newExpiresAt = Date.now() + 60 * 60 * 1000;
|
|
|
|
fstoken.value = newToken;
|
|
roots.value = newRoots;
|
|
expiresAt.value = newExpiresAt;
|
|
|
|
saveStored({ fstoken: newToken, roots: newRoots, expiresAt: newExpiresAt });
|
|
} catch {}
|
|
}
|
|
|
|
function invalidate() {
|
|
fstoken.value = '';
|
|
roots.value = [];
|
|
expiresAt.value = 0;
|
|
clearStored();
|
|
}
|
|
|
|
return { fstoken, roots, ready, acquire, invalidate };
|
|
});
|