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