SkateSpots/src/hooks/useCVMServerHealth.ts
SkateSpots 3f28c3fdb4
Some checks are pending
Deploy to GitHub Pages / deploy (push) Waiting to run
security: rotate CVM identity and externalize signing
2026-11-01 17:22:15 +07:00

112 lines
3.8 KiB
TypeScript

/**
* useCVMServerHealth
* ─────────────────────────────────────────────────────────────────────────────
* Owner-only health probe for the SkateSpots ContextVM (CVM) MCP server.
*
* A ContextVM server lives as a Nostr identity that publishes a server
* announcement and listens for encrypted MCP requests on the ContextVM relay.
* The most reliable "is it alive?" signal we can read from the browser is the
* server's own events on that relay: the announcement (kind 31990) plus any
* newer heartbeat/activity. We report status + how long ago the server was
* last seen.
*
* NOTE: the user-facing Sasquatch data layer (`useCVMClient`) queries the data
* relays DIRECTLY and does NOT depend on this server being up. This probe is
* purely an operational status check for platform owners.
*/
import { useCallback, useState } from 'react';
import { useNostr } from '@nostrify/react';
/** The SkateSpots CVM server identity (see contextvm.org/s/<pubkey>). */
export const CVM_SERVER_PUBKEY =
'940bf0e4fbc64b6c0f09d48c3264723c524a31b5ad0d1aaec361263731689d95';
/** The relay the CVM server announces on. */
export const CVM_RELAY = 'wss://relay.contextvm.org';
/**
* Freshness threshold. The server re-publishes a heartbeat announcement every
* ~15 min (`CVM_HEARTBEAT_MS`), so allow several missed beats before calling it
* stale — otherwise a healthy server reads "stale" between restarts.
*/
const STALE_AFTER_MS = 45 * 60 * 1000; // 45 minutes
export type CVMHealthStatus = 'idle' | 'checking' | 'online' | 'stale' | 'offline' | 'error';
export interface CVMHealthResult {
status: CVMHealthStatus;
/** Unix seconds of the newest event authored by the server, if any. */
lastSeen: number | null;
/** Number of server-authored events found on the probe. */
eventCount: number;
/** Human-readable error, when status === 'error'. */
error: string | null;
/** When the probe last ran (ms epoch). */
checkedAt: number | null;
}
const INITIAL: CVMHealthResult = {
status: 'idle',
lastSeen: null,
eventCount: 0,
error: null,
checkedAt: null,
};
export function useCVMServerHealth() {
const { nostr } = useNostr();
const [result, setResult] = useState<CVMHealthResult>(INITIAL);
const check = useCallback(async () => {
setResult((r) => ({ ...r, status: 'checking', error: null }));
try {
// Probe the ContextVM relay directly for anything the server has authored.
// We ask for a small window across the kinds a CVM server emits:
// 31990 (handler/announcement), plus recent activity of any kind.
const relay = nostr.relay(CVM_RELAY);
const signal = AbortSignal.timeout(8000);
const events = await relay.query(
[{ authors: [CVM_SERVER_PUBKEY], limit: 25 }],
{ signal },
);
if (!events.length) {
setResult({
status: 'offline',
lastSeen: null,
eventCount: 0,
error: null,
checkedAt: Date.now(),
});
return;
}
const lastSeen = Math.max(...events.map((e) => e.created_at));
const ageMs = Date.now() - lastSeen * 1000;
const status: CVMHealthStatus = ageMs <= STALE_AFTER_MS ? 'online' : 'stale';
setResult({
status,
lastSeen,
eventCount: events.length,
error: null,
checkedAt: Date.now(),
});
} catch (err) {
setResult({
status: 'error',
lastSeen: null,
eventCount: 0,
error: err instanceof Error ? err.message : 'Could not reach the ContextVM relay',
checkedAt: Date.now(),
});
}
}, [nostr]);
const reset = useCallback(() => setResult(INITIAL), []);
return { ...result, check, reset };
}