File size: 8,085 Bytes
674d56a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | /**
* Hermes Dashboard Plugin SDK — typed contract (SPIKE)
* ====================================================
*
* This is the public type surface for ``window.__HERMES_PLUGIN_SDK__`` and
* ``window.__HERMES_PLUGINS__``, the globals the dashboard host exposes to
* plugin bundles (see ``web/src/plugins/registry.ts::exposePluginSDK``).
*
* STATUS: spike. This file documents the contract and gives plugin authors
* (in-repo IIFEs and external bundles alike) editor types without bundling
* their own copies of React / the API client. It is intentionally a
* hand-authored ambient declaration rather than ``typeof
* window.__HERMES_PLUGIN_SDK__`` because:
* 1. The runtime object is assembled from many internal modules
* (``@/lib/api``, ``@nous-research/ui``, …). Deriving the type would
* leak those internal import paths into the public contract and couple
* external plugins to the host's internal module layout.
* 2. A hand-authored contract is the *versioned API boundary* — changing
* it is a deliberate act, visible in review, not an accidental
* consequence of refactoring an internal helper.
*
* Versioning: bump ``HermesPluginSDK["sdkVersion"]`` (and the
* ``SDK_CONTRACT_VERSION`` const the host exposes) on any
* backwards-incompatible change to this surface. Additive changes
* (new optional fields, new helpers) don't require a major bump.
*
* OPEN QUESTIONS for productionising this spike (do not block the auth fix):
* - Ship as a published ``@hermes/dashboard-plugin-sdk`` types package, or
* keep in-repo and copy into external plugin repos?
* - Should the host assert at runtime that a plugin's declared
* ``manifest.sdk_version`` is compatible before executing it?
* - The ``components`` map is typed loosely as ``Record<string,
* ComponentType>`` here; do we want exact per-component prop types
* (pulls @nous-research/ui types into the contract) or is the loose
* shape the right boundary for external authors?
*/
import type { ComponentType } from "react";
// ---------------------------------------------------------------------------
// Auth-relevant helpers (the surface this PR adds/sanctions)
// ---------------------------------------------------------------------------
/**
* JSON ``fetch`` for dashboard ``/api/...`` endpoints. Handles auth in both
* modes (loopback session-token header / gated cookie), throws
* ``Error("<status>: <body>")`` on non-2xx, and triggers the global
* 401 → /login redirect in gated mode. Use for all JSON plugin endpoints.
*/
export type FetchJSON = <T = unknown>(
url: string,
init?: RequestInit,
options?: { allowUnauthorized?: boolean },
) => Promise<T>;
/**
* Authenticated ``fetch`` for NON-JSON endpoints (uploads via ``FormData``,
* binary/blob downloads). Same auth handling as ``fetchJSON`` but returns
* the raw ``Response``, does not parse, does not throw on non-2xx, and does
* not run the 401 redirect. Plugins MUST use this (or ``fetchJSON``) instead
* of calling ``fetch`` with a hand-read ``window.__HERMES_SESSION_TOKEN__``.
*/
export type AuthedFetch = (url: string, init?: RequestInit) => Promise<Response>;
/**
* Build an absolute ``ws(s)://`` URL for a dashboard WebSocket endpoint with
* the correct auth query param for the active mode (single-use ``ticket`` in
* gated OAuth mode, ``token`` in loopback). Plugins MUST use this for any
* WebSocket instead of hand-assembling the URL + reading the session token.
*/
export type BuildWsUrl = (
path: string,
params?: Record<string, string>,
) => Promise<string>;
/** Lower-level: just the ``[authParamName, authParamValue]`` pair. */
export type BuildWsAuthParam = () => Promise<[string, string]>;
// ---------------------------------------------------------------------------
// Registry surface (window.__HERMES_PLUGINS__)
// ---------------------------------------------------------------------------
export interface PluginRegistry {
/** Register the plugin's main tab component by manifest name. */
register(name: string, component: ComponentType<Record<string, never>>): void;
/** Register a component into a named host slot. */
registerSlot(slot: string, name: string, component: ComponentType): void;
}
// ---------------------------------------------------------------------------
// SDK surface (window.__HERMES_PLUGIN_SDK__)
// ---------------------------------------------------------------------------
export interface HermesPluginSDK {
/** Contract version of this SDK surface (see SDK_CONTRACT_VERSION). */
readonly sdkVersion: string;
/** React core — use instead of importing/bundling react. */
React: typeof import("react").default;
hooks: {
useState: typeof import("react").useState;
useEffect: typeof import("react").useEffect;
useCallback: typeof import("react").useCallback;
useMemo: typeof import("react").useMemo;
useRef: typeof import("react").useRef;
useContext: typeof import("react").useContext;
createContext: typeof import("react").createContext;
/**
* Toast feedback. Returns ``{ showToast, toast }`` where ``toast`` is the
* current visible toast (or null) and ``showToast(message, 'success' | 'error')``
* replaces it. Mount the ``Toast`` component (also on ``components``) somewhere
* in your plugin's tree to render the visible toast.
*/
useToast: () => {
showToast: (message: string, type: "success" | "error") => void;
toast: { message: string; type: "success" | "error" } | null;
};
/**
* Single-id delete-confirm state machine. Pass ``onDelete(id)`` (an async
* function). Returns ``{ requestDelete, confirm, cancel, isOpen, isDeleting,
* pendingId }``. Pair with the ``ConfirmDialog`` primitive (passed as
* ``open={isOpen}`` etc.) or any custom dialog.
*/
useConfirmDelete: <TId,>(opts: { onDelete: (id: TId) => Promise<void> }) => {
requestDelete: (id: TId) => void;
confirm: () => Promise<void>;
cancel: () => void;
isOpen: boolean;
isDeleting: boolean;
pendingId: TId | null;
};
};
/**
* Typed convenience client for core dashboard endpoints. Typed permissively
* at the boundary (methods vary in arity and return type — most return
* ``Promise<T>``, a few return a URL string synchronously); plugins call the
* specific methods they need. See ``web/src/lib/api.ts`` for the concrete shape.
*/
api: Record<string, (...args: never[]) => unknown>;
/** JSON fetch with host auth handling. */
fetchJSON: FetchJSON;
/** Authenticated raw fetch for uploads / blob downloads. */
authedFetch: AuthedFetch;
/** Build an auth'd WebSocket URL for the active mode. */
buildWsUrl: BuildWsUrl;
/** Resolve just the WS auth query-param pair. */
buildWsAuthParam: BuildWsAuthParam;
/**
* Shared UI primitives (Nous DS / shadcn). Typed permissively at the
* boundary: the host's concrete components (some of which require props like
* ``active``/``value``/``name``) must be assignable here, and external plugin
* authors render them dynamically without the host's internal prop types.
* ``ComponentType<never>`` accepts any component regardless of its prop
* requirements (props are contravariant).
*/
components: Record<string, ComponentType<never>>;
utils: {
cn: (...classes: Array<string | false | null | undefined>) => string;
/** Relative-time formatter. Accepts an epoch-ms number. */
timeAgo: (ts: number) => string;
/** Relative-time formatter for an ISO-8601 string. */
isoTimeAgo: (iso: string) => string;
};
/**
* i18n hook. Returns the host's i18n context value; typed loosely at the
* boundary so the contract doesn't couple to the host's internal
* ``I18nContextValue`` shape. Plugins typically call ``useI18n().t(...)``.
*/
useI18n: () => unknown;
}
declare global {
interface Window {
__HERMES_PLUGIN_SDK__?: HermesPluginSDK;
__HERMES_PLUGINS__?: PluginRegistry;
}
}
export {};
|