@wrongstack/webui 0.63.4 → 0.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  2. package/dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  3. package/dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  4. package/dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  5. package/dist/assets/ibm-plex-mono-cyrillic-600-normal-CTOM6hUh.woff2 +0 -0
  6. package/dist/assets/ibm-plex-mono-cyrillic-600-normal-fLZuRloM.woff +0 -0
  7. package/dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  8. package/dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  9. package/dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  10. package/dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  11. package/dist/assets/ibm-plex-mono-cyrillic-ext-600-normal-9HEixskS.woff +0 -0
  12. package/dist/assets/ibm-plex-mono-cyrillic-ext-600-normal-V-xxqcpd.woff2 +0 -0
  13. package/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  14. package/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  15. package/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  16. package/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  17. package/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
  18. package/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
  19. package/dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  20. package/dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  21. package/dist/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  22. package/dist/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  23. package/dist/assets/ibm-plex-mono-latin-ext-600-normal-D38SheWl.woff2 +0 -0
  24. package/dist/assets/ibm-plex-mono-latin-ext-600-normal-DmB0ttJJ.woff +0 -0
  25. package/dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  26. package/dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  27. package/dist/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  28. package/dist/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  29. package/dist/assets/ibm-plex-mono-vietnamese-600-normal-D2EvbN8M.woff2 +0 -0
  30. package/dist/assets/ibm-plex-mono-vietnamese-600-normal-iLQfcSjf.woff +0 -0
  31. package/dist/assets/ibm-plex-sans-cyrillic-ext-wght-normal-d45eAU9y.woff2 +0 -0
  32. package/dist/assets/ibm-plex-sans-cyrillic-wght-normal-BAAhND-U.woff2 +0 -0
  33. package/dist/assets/ibm-plex-sans-greek-wght-normal-CmyJS8uq.woff2 +0 -0
  34. package/dist/assets/ibm-plex-sans-latin-ext-wght-normal-CIII54If.woff2 +0 -0
  35. package/dist/assets/ibm-plex-sans-latin-wght-normal-IvpUvPa2.woff2 +0 -0
  36. package/dist/assets/ibm-plex-sans-vietnamese-wght-normal-Dg1JeJN0.woff2 +0 -0
  37. package/dist/assets/index-BeXRAkSS.js +94 -0
  38. package/dist/assets/index-C_0-qbQ-.css +1 -0
  39. package/dist/assets/{vendor-oYD55Pw4.js → vendor-CzdG0ns2.js} +88 -88
  40. package/dist/assets/vendor-DW1jimNH.css +1 -0
  41. package/dist/index.css +333 -214
  42. package/dist/index.css.map +1 -1
  43. package/dist/index.html +4 -3
  44. package/dist/index.js +2769 -2832
  45. package/dist/index.js.map +1 -1
  46. package/dist/server/entry.js +479 -255
  47. package/dist/server/entry.js.map +1 -1
  48. package/dist/server/index.d.ts +298 -2
  49. package/dist/server/index.js +480 -225
  50. package/dist/server/index.js.map +1 -1
  51. package/package.json +9 -6
  52. package/dist/assets/index-5ECutVTP.css +0 -1
  53. package/dist/assets/index-BRHGqfHg.js +0 -94
@@ -1,5 +1,15 @@
1
- import { Agent, EventBus, SessionWriter, ToolRegistry, ModelsRegistry, ConfigStore, SecretVault } from '@wrongstack/core';
1
+ import { WebSocket } from 'ws';
2
+ import { Agent, EventBus, SessionWriter, ToolRegistry, ModelsRegistry, ConfigStore, SecretVault, ProviderConfig, ProviderApiKey } from '@wrongstack/core';
3
+ import * as http from 'node:http';
2
4
 
5
+ interface WSServerMessage {
6
+ type: string;
7
+ payload: unknown;
8
+ }
9
+ interface WSClientMessage {
10
+ type: string;
11
+ payload?: unknown;
12
+ }
3
13
  interface WebUIOptions {
4
14
  port?: number;
5
15
  webuiPort?: number;
@@ -15,10 +25,296 @@ interface BackendServices {
15
25
  globalConfigPath: string;
16
26
  projectRoot: string;
17
27
  }
28
+ interface ConnectedClient {
29
+ ws: WebSocket;
30
+ sessionId: string | null;
31
+ connectedAt: number;
32
+ }
33
+
34
+ interface CreateHttpServerOptions {
35
+ /** Port to listen on. Defaults to 3456 (or the `PORT` env var). */
36
+ port?: number;
37
+ /** Host/interface to bind. Typically the loopback for the WebUI. */
38
+ host: string;
39
+ /** Resolved path to the directory containing the built React assets. */
40
+ distDir: string;
41
+ /**
42
+ * WS port — appears in the CSP `connect-src` directive so the browser
43
+ * is allowed to open a WebSocket back to the local server.
44
+ */
45
+ wsPort: number;
46
+ }
47
+ /**
48
+ * Inject the live WS port into the served HTML so the frontend connects to
49
+ * THIS instance's backend instead of a hardcoded default. Enables running
50
+ * several WebUI instances simultaneously on different PORT/WS_PORT pairs
51
+ * (e.g. one per project) — each instance serves HTML stamped with its own
52
+ * WS port.
53
+ *
54
+ * A `<meta>` tag is used deliberately rather than an inline `<script>`: the
55
+ * CSP sets `script-src 'self'`, which would block an inline script, but meta
56
+ * tags are not subject to script-src. The frontend reads
57
+ * `meta[name="wrongstack-ws-port"]` (see ws-client.ts `defaultWsUrl`).
58
+ */
59
+ declare function injectWsPort(html: string, wsPort: number): string;
60
+ /** Build the Content-Security-Policy value for the given WS port. */
61
+ declare function buildCspHeader(wsPort: number): string;
62
+ /**
63
+ * Create the static-file HTTP server. Returns the `http.Server` (not
64
+ * listening yet) so the caller can attach to a `shutdown()` hook and
65
+ * coordinate the listen() with the WebSocket bootstrap.
66
+ */
67
+ declare function createHttpServer(opts: CreateHttpServerOptions): http.Server;
68
+
69
+ /**
70
+ * Free-port discovery for the standalone WebUI server.
71
+ *
72
+ * When a user runs several instances, the default ports (HTTP 3456 / WS 3457)
73
+ * are taken by the first one. Rather than make the user hand-pick `PORT` /
74
+ * `WS_PORT` for every extra instance, the server probes upward from the
75
+ * requested port and binds the first free one — then stamps that real port into
76
+ * the served HTML and the instance registry so everything stays consistent.
77
+ *
78
+ * The probe binds a throwaway `net.Server`, then closes it, so there is a tiny
79
+ * TOCTOU window between "found free" and "the real server binds it". For local
80
+ * single-user multi-instance use that race is negligible; if it ever loses, the
81
+ * real bind fails loudly with EADDRINUSE exactly as before.
82
+ */
83
+ /** Resolve true when `port` can be bound on `host`, false on EADDRINUSE/EACCES. */
84
+ declare function isPortFree(host: string, port: number): Promise<boolean>;
85
+ interface FindFreePortOptions {
86
+ /** Ports to skip even if free (e.g. one already chosen for the sibling server). */
87
+ exclude?: Set<number>;
88
+ /** How many consecutive ports to try before giving up. Default 200. */
89
+ maxTries?: number;
90
+ }
91
+ /**
92
+ * Find the first free port at or above `startPort` on `host`, skipping any in
93
+ * `exclude`. Throws if nothing is free within `maxTries` steps.
94
+ */
95
+ declare function findFreePort(host: string, startPort: number, opts?: FindFreePortOptions): Promise<number>;
96
+
97
+ /**
98
+ * Best-effort "open this URL in the default browser" for `--webui --open`.
99
+ *
100
+ * Cross-platform via the OS opener (`start` / `open` / `xdg-open`). Fully
101
+ * fire-and-forget: a missing opener, a headless box, or a spawn failure must
102
+ * NEVER take the server down — the URL is always also printed to the console.
103
+ */
104
+ /** Resolve the platform's URL-opener command + args. */
105
+ declare function browserOpenCommand(url: string, platform?: NodeJS.Platform): {
106
+ command: string;
107
+ args: string[];
108
+ };
109
+ /** Spawn the OS browser-opener for `url`. Never throws. */
110
+ declare function openBrowser(url: string, platform?: NodeJS.Platform): void;
111
+
112
+ /**
113
+ * Running-instance registry for the standalone WebUI server.
114
+ *
115
+ * Every live `webui` process records itself in a single JSON file under the
116
+ * wstack home dir (`~/.wrongstack/webui-instances.json`) so a user running
117
+ * several instances (one per project, or several per project on different
118
+ * ports) can see at a glance which ports are open for which path.
119
+ *
120
+ * Design notes:
121
+ * - **Self-healing**: every register/unregister/list prunes entries whose PID
122
+ * is no longer alive (`process.kill(pid, 0)`), so a crashed instance that
123
+ * never got to unregister doesn't leave a ghost behind.
124
+ * - **Atomic writes**: the file is rewritten via `atomicWrite` (tmp + rename),
125
+ * so a concurrent reader never sees a half-written file. Two instances
126
+ * starting at the *exact* same millisecond could still race the
127
+ * read-modify-write — acceptable for a best-effort tracking file, and the
128
+ * next register() heals any dropped entry.
129
+ * - **Best-effort**: a failure to read/write the registry must NEVER take the
130
+ * server down. Callers wrap these in `.catch()`.
131
+ */
132
+ /** One running WebUI process. */
133
+ interface WebUIInstanceRecord {
134
+ /** OS process id — also the liveness key. */
135
+ pid: number;
136
+ /** HTTP port serving the React frontend. */
137
+ httpPort: number;
138
+ /** WebSocket port for the agent backend. */
139
+ wsPort: number;
140
+ /** Bind host (e.g. 127.0.0.1 or 0.0.0.0). */
141
+ host: string;
142
+ /** Absolute project root the instance booted against. */
143
+ projectRoot: string;
144
+ /** Display name (basename of projectRoot). */
145
+ projectName: string;
146
+ /** ISO timestamp when the instance registered. */
147
+ startedAt: string;
148
+ /** Convenience open-in-browser URL. */
149
+ url: string;
150
+ }
151
+ /** Default wstack home dir (`~/.wrongstack`). Callers may override the base. */
152
+ declare function defaultBaseDir(): string;
153
+ /** Resolve the registry file path for a given base dir. */
154
+ declare function registryPath(baseDir?: string): string;
155
+ /**
156
+ * Register (or refresh) this instance. Prunes dead entries and any stale entry
157
+ * for our own PID before adding the current record. Best-effort — rejects only
158
+ * on a hard fs error, which callers swallow.
159
+ */
160
+ declare function registerInstance(record: WebUIInstanceRecord, baseDir?: string): Promise<void>;
161
+ /** Remove this instance (called on graceful shutdown). Also prunes dead pids. */
162
+ declare function unregisterInstance(pid: number, baseDir?: string): Promise<void>;
163
+ /** List live instances, pruning any dead entries encountered. */
164
+ declare function listInstances(baseDir?: string): Promise<WebUIInstanceRecord[]>;
165
+ /** Human-readable table of running instances for `webui --list`. */
166
+ declare function formatInstances(instances: WebUIInstanceRecord[]): string;
167
+
168
+ /**
169
+ * Send a JSON message to a single WebSocket client.
170
+ * No-op when the socket is not in OPEN state (disconnected / closing).
171
+ */
172
+ declare function send(ws: WebSocket, msg: WSServerMessage): void;
173
+ /**
174
+ * Broadcast a JSON message to every connected client.
175
+ * Swallows per-socket send errors — a client that disconnected between the
176
+ * readyState check and `ws.send()` is cleaned up by its own `close` handler.
177
+ */
178
+ declare function broadcast(clients: Map<WebSocket, ConnectedClient>, msg: WSServerMessage): void;
179
+ /**
180
+ * Send a success/failure result message (used by key.* and provider.* handlers).
181
+ * The frontend expects `key.operation_result` with `{ success, message }`.
182
+ */
183
+ declare function sendResult(ws: WebSocket, success: boolean, message: string): void;
184
+ /**
185
+ * Extract a human-readable message from an unknown thrown value.
186
+ */
187
+ declare function errMessage(err: unknown): string;
188
+ /**
189
+ * Generate a cryptographically random WebSocket auth token (hex string).
190
+ * Shared between standalone and CLI-embedded WebUI servers.
191
+ */
192
+ declare function generateAuthToken(): string;
193
+
194
+ /** A hostname that refers to the local machine. */
195
+ declare function isLoopbackHostname(hostname: string): boolean;
196
+ /** True when the server is bound to a loopback interface (vs. LAN/0.0.0.0). */
197
+ declare function isLoopbackBind(wsHost: string): boolean;
198
+ /**
199
+ * Constant-time comparison of a provided token against the expected one.
200
+ * A length mismatch short-circuits (lengths aren't secret); equal-length
201
+ * inputs are compared with `timingSafeEqual` so the token can't be recovered
202
+ * byte-by-byte via response timing.
203
+ */
204
+ declare function tokenMatches(provided: string | undefined, expected: string): boolean;
205
+ /** Pull the `token` query param out of a request URL (`/?token=…`). */
206
+ declare function extractToken(url: string): string | undefined;
207
+ /**
208
+ * DNS-rebinding defense. On a loopback bind, the `Host` header must resolve to
209
+ * a loopback name. When the operator deliberately exposes the socket (wsHost is
210
+ * a LAN/0.0.0.0 address) the Host is legitimately non-loopback, so the guard is
211
+ * skipped and connection auth falls to the token check.
212
+ */
213
+ declare function hostHeaderOk(input: {
214
+ hostHeader: string | undefined;
215
+ wsHost: string;
216
+ }): boolean;
217
+ interface VerifyClientInput {
218
+ /** Browser `Origin` header, or undefined for non-browser clients. */
219
+ origin?: string;
220
+ /** Request URL (`req.url`) — carries the `?token=…` query param. */
221
+ url: string;
222
+ /** `Host` header (`req.headers.host`). */
223
+ hostHeader?: string;
224
+ /** Peer address (`req.socket.remoteAddress`). */
225
+ remoteAddress?: string;
226
+ /** Host/interface the WS server is bound to. */
227
+ wsHost: string;
228
+ /** The server's generated auth token. */
229
+ expectedToken: string;
230
+ }
231
+ /**
232
+ * Decide whether to accept an incoming WebSocket handshake. Pure mirror of the
233
+ * closure previously inlined in `index.ts`; see the module doc for the layered
234
+ * policy. Returns `true` to accept, `false` to reject.
235
+ */
236
+ declare function verifyClient(input: VerifyClientInput): boolean;
237
+
238
+ /**
239
+ * Pure provider/API-key record transforms for the WebUI server's `key.*` and
240
+ * `provider.*` WebSocket handlers.
241
+ *
242
+ * These operate on an in-memory `providers` record (the decrypted
243
+ * `config.providers` map) and return a `{ ok, message }` result mirroring the
244
+ * status string the handler sends back to the client. All persistence
245
+ * (load/decrypt, encrypt/atomic-write) and WS messaging stays in `index.ts` —
246
+ * keeping this layer pure means the security-sensitive key bookkeeping (which
247
+ * key is active, when a provider is dropped, how legacy single-key configs are
248
+ * normalized) is unit-testable without a vault or a socket.
249
+ *
250
+ * Extracted from `index.ts`; transforms mutate the passed record in place, the
251
+ * same way the original handlers did before calling `saveProviders`.
252
+ */
253
+
254
+ type ProvidersRecord = Record<string, ProviderConfig>;
255
+ interface KeyOpResult {
256
+ ok: boolean;
257
+ message: string;
258
+ }
259
+ /**
260
+ * Normalize a provider's keys to the array form, upgrading a legacy single
261
+ * `apiKey` string to a one-element `[{ label: 'default', ... }]` list. Returns
262
+ * fresh copies so callers can mutate without aliasing the stored config.
263
+ */
264
+ declare function normalizeKeys(cfg: ProviderConfig): ProviderApiKey[];
265
+ /**
266
+ * Write a normalized key list back onto a provider config: drop all key fields
267
+ * when empty, otherwise sync `apiKeys`, the legacy `apiKey` mirror (the active
268
+ * key), and re-point `activeKey` if it no longer names a present key.
269
+ */
270
+ declare function writeKeysBack(cfg: ProviderConfig, keys: ProviderApiKey[]): void;
271
+ /** Mask a secret for display: `••••` for short keys, `abcd…wxyz` otherwise. */
272
+ declare function maskedKey(key: string | undefined): string;
273
+ /** Add or replace a labeled key for a provider, creating the provider if new. */
274
+ declare function upsertKey(providers: ProvidersRecord, providerId: string, label: string, apiKey: string, nowIso: string): KeyOpResult;
275
+ /** Remove a labeled key; drops the provider entirely when its last key goes. */
276
+ declare function deleteKey(providers: ProvidersRecord, providerId: string, label: string): KeyOpResult;
277
+ /** Point a provider's active key at the given label. */
278
+ declare function setActiveKey(providers: ProvidersRecord, providerId: string, label: string): KeyOpResult;
279
+ /** Register a brand-new provider (optionally with an initial `default` key). */
280
+ declare function addProvider(providers: ProvidersRecord, payload: {
281
+ id: string;
282
+ family: string;
283
+ baseUrl?: string;
284
+ apiKey?: string;
285
+ }, nowIso: string): KeyOpResult;
286
+ /** Remove an entire provider and all its keys. */
287
+ declare function removeProvider(providers: ProvidersRecord, providerId: string): KeyOpResult;
288
+
289
+ /**
290
+ * Read the `providers` section from the global config, decrypting
291
+ * secret-bearing fields. Returns an empty record when the config file
292
+ * doesn't exist or has no `providers` key.
293
+ */
294
+ declare function loadSavedProviders(configPath: string, vault: SecretVault): Promise<Record<string, ProviderConfig>>;
295
+ /**
296
+ * Write `providers` back into the global config, encrypting secrets first.
297
+ * Refuses to overwrite a corrupt-but-existing config file (the operator
298
+ * should fix it manually). When the config file is missing (ENOENT), starts
299
+ * from an empty object.
300
+ */
301
+ declare function saveProviders(configPath: string, vault: SecretVault, providers: Record<string, ProviderConfig>): Promise<void>;
302
+ /**
303
+ * Small helper for the standalone WebUI entry point: create a
304
+ * `{ load, save }` pair from a config path alone (uses the
305
+ * config-directory-relative `.key` file for the vault). The `--webui`
306
+ * CLI mode and the standalone server both need to read/write the
307
+ * `providers` map identically.
308
+ */
309
+ declare function createProviderConfigIO(configPath: string): {
310
+ load: () => Promise<Record<string, ProviderConfig>>;
311
+ save: (providers: Record<string, ProviderConfig>) => Promise<void>;
312
+ };
18
313
 
19
314
  declare function startWebUI(opts?: {
20
315
  wsPort?: number;
21
316
  wsHost?: string;
317
+ open?: boolean;
22
318
  }): Promise<void>;
23
319
 
24
- export { type BackendServices, type WebUIOptions, startWebUI };
320
+ export { type BackendServices, type ConnectedClient, type KeyOpResult, type ProvidersRecord, type VerifyClientInput, type WSClientMessage, type WSServerMessage, type WebUIInstanceRecord, type WebUIOptions, addProvider, broadcast, browserOpenCommand, buildCspHeader, createHttpServer, createProviderConfigIO, defaultBaseDir, deleteKey, errMessage, extractToken, findFreePort, formatInstances, generateAuthToken, hostHeaderOk, injectWsPort, isLoopbackBind, isLoopbackHostname, isPortFree, listInstances, loadSavedProviders, maskedKey, normalizeKeys, openBrowser, registerInstance, registryPath, removeProvider, saveProviders, send, sendResult, setActiveKey, startWebUI, tokenMatches, unregisterInstance, upsertKey, verifyClient, writeKeysBack };