This repository has been archived on 2026-04-03. You can view files and clone it, but cannot push or open issues or pull requests.
nyx/src/store/viewer.ts
Nico e2667f8e12 Initial nyx project — fork of hermes-frontend
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>
2026-03-31 20:23:27 +02:00

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 };
});