@zackbart/connecta 0.7.9 → 0.8.1

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 (199) hide show
  1. package/CHANGELOG.md +135 -0
  2. package/README.md +47 -148
  3. package/dist/activity.d.ts +0 -8
  4. package/dist/activity.d.ts.map +1 -1
  5. package/dist/activity.js +0 -1
  6. package/dist/activity.js.map +1 -1
  7. package/dist/auth/bearer.d.ts +1 -7
  8. package/dist/auth/bearer.d.ts.map +1 -1
  9. package/dist/auth/bearer.js +3 -24
  10. package/dist/auth/bearer.js.map +1 -1
  11. package/dist/auth/clerk.d.ts +2 -11
  12. package/dist/auth/clerk.d.ts.map +1 -1
  13. package/dist/auth/clerk.js +31 -46
  14. package/dist/auth/clerk.js.map +1 -1
  15. package/dist/call-admission.d.ts.map +1 -1
  16. package/dist/call-admission.js +9 -4
  17. package/dist/call-admission.js.map +1 -1
  18. package/dist/catalog-fingerprint.d.ts +20 -0
  19. package/dist/catalog-fingerprint.d.ts.map +1 -0
  20. package/dist/catalog-fingerprint.js +34 -0
  21. package/dist/catalog-fingerprint.js.map +1 -0
  22. package/dist/catalog-limits.d.ts +10 -0
  23. package/dist/catalog-limits.d.ts.map +1 -0
  24. package/dist/catalog-limits.js +10 -0
  25. package/dist/catalog-limits.js.map +1 -0
  26. package/dist/catalog-service.d.ts +137 -0
  27. package/dist/catalog-service.d.ts.map +1 -0
  28. package/dist/catalog-service.js +401 -0
  29. package/dist/catalog-service.js.map +1 -0
  30. package/dist/catalog.d.ts +6 -0
  31. package/dist/catalog.d.ts.map +1 -1
  32. package/dist/catalog.js +53 -2
  33. package/dist/catalog.js.map +1 -1
  34. package/dist/concurrency.d.ts +4 -0
  35. package/dist/concurrency.d.ts.map +1 -0
  36. package/dist/concurrency.js +31 -0
  37. package/dist/concurrency.js.map +1 -0
  38. package/dist/connectors/api.d.ts.map +1 -1
  39. package/dist/connectors/api.js +22 -12
  40. package/dist/connectors/api.js.map +1 -1
  41. package/dist/connectors/remote-mcp.d.ts.map +1 -1
  42. package/dist/connectors/remote-mcp.js +53 -41
  43. package/dist/connectors/remote-mcp.js.map +1 -1
  44. package/dist/credentials.d.ts +3 -2
  45. package/dist/credentials.d.ts.map +1 -1
  46. package/dist/errors.d.ts +19 -1
  47. package/dist/errors.d.ts.map +1 -1
  48. package/dist/errors.js.map +1 -1
  49. package/dist/execute.d.ts +10 -16
  50. package/dist/execute.d.ts.map +1 -1
  51. package/dist/execute.js +143 -330
  52. package/dist/execute.js.map +1 -1
  53. package/dist/executor-result.d.ts +0 -3
  54. package/dist/executor-result.d.ts.map +1 -1
  55. package/dist/executor-result.js +3 -3
  56. package/dist/executor-result.js.map +1 -1
  57. package/dist/executors/quickjs-child.js +3 -3
  58. package/dist/executors/quickjs-child.js.map +1 -1
  59. package/dist/executors/quickjs-protocol.d.ts +4 -1
  60. package/dist/executors/quickjs-protocol.d.ts.map +1 -1
  61. package/dist/executors/quickjs-runtime.d.ts.map +1 -1
  62. package/dist/executors/quickjs-runtime.js +7 -2
  63. package/dist/executors/quickjs-runtime.js.map +1 -1
  64. package/dist/executors/quickjs.d.ts.map +1 -1
  65. package/dist/executors/quickjs.js +4 -1
  66. package/dist/executors/quickjs.js.map +1 -1
  67. package/dist/favicon.js +2 -2
  68. package/dist/favicon.js.map +1 -1
  69. package/dist/index.d.ts +8 -82
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +58 -104
  72. package/dist/index.js.map +1 -1
  73. package/dist/invocation.d.ts +101 -0
  74. package/dist/invocation.d.ts.map +1 -0
  75. package/dist/invocation.js +405 -0
  76. package/dist/invocation.js.map +1 -0
  77. package/dist/meta-tools.d.ts +9 -95
  78. package/dist/meta-tools.d.ts.map +1 -1
  79. package/dist/meta-tools.js +235 -725
  80. package/dist/meta-tools.js.map +1 -1
  81. package/dist/operator-ui/generated.d.ts +3 -0
  82. package/dist/operator-ui/generated.d.ts.map +1 -0
  83. package/dist/operator-ui/generated.js +5 -0
  84. package/dist/operator-ui/generated.js.map +1 -0
  85. package/dist/operator-ui/model.d.ts +73 -0
  86. package/dist/operator-ui/model.d.ts.map +1 -0
  87. package/dist/operator-ui/model.js +28 -0
  88. package/dist/operator-ui/model.js.map +1 -0
  89. package/dist/registry.d.ts +33 -184
  90. package/dist/registry.d.ts.map +1 -1
  91. package/dist/registry.js +345 -330
  92. package/dist/registry.js.map +1 -1
  93. package/dist/retired-toolkits.d.ts +3 -0
  94. package/dist/retired-toolkits.d.ts.map +1 -0
  95. package/dist/retired-toolkits.js +9 -0
  96. package/dist/retired-toolkits.js.map +1 -0
  97. package/dist/routes/activity.d.ts +3 -0
  98. package/dist/routes/activity.d.ts.map +1 -0
  99. package/dist/routes/activity.js +170 -0
  100. package/dist/routes/activity.js.map +1 -0
  101. package/dist/routes/credentials.d.ts +3 -0
  102. package/dist/routes/credentials.d.ts.map +1 -0
  103. package/dist/routes/credentials.js +170 -0
  104. package/dist/routes/credentials.js.map +1 -0
  105. package/dist/routes/mcp.d.ts +8 -0
  106. package/dist/routes/mcp.d.ts.map +1 -0
  107. package/dist/routes/mcp.js +270 -0
  108. package/dist/routes/mcp.js.map +1 -0
  109. package/dist/routes/oauth.d.ts +4 -0
  110. package/dist/routes/oauth.d.ts.map +1 -0
  111. package/dist/routes/oauth.js +263 -0
  112. package/dist/routes/oauth.js.map +1 -0
  113. package/dist/routes/shared.d.ts +76 -0
  114. package/dist/routes/shared.d.ts.map +1 -0
  115. package/dist/routes/shared.js +140 -0
  116. package/dist/routes/shared.js.map +1 -0
  117. package/dist/routes/ui.d.ts +3 -0
  118. package/dist/routes/ui.d.ts.map +1 -0
  119. package/dist/routes/ui.js +116 -0
  120. package/dist/routes/ui.js.map +1 -0
  121. package/dist/server.d.ts +9 -40
  122. package/dist/server.d.ts.map +1 -1
  123. package/dist/server.js +62 -1322
  124. package/dist/server.js.map +1 -1
  125. package/dist/skills.d.ts +3 -21
  126. package/dist/skills.d.ts.map +1 -1
  127. package/dist/skills.js +15 -43
  128. package/dist/skills.js.map +1 -1
  129. package/dist/storage/file.d.ts.map +1 -1
  130. package/dist/storage/file.js +3 -1
  131. package/dist/storage/file.js.map +1 -1
  132. package/dist/storage/memory.d.ts.map +1 -1
  133. package/dist/storage/memory.js +3 -1
  134. package/dist/storage/memory.js.map +1 -1
  135. package/dist/timeout.d.ts.map +1 -1
  136. package/dist/timeout.js +2 -4
  137. package/dist/timeout.js.map +1 -1
  138. package/dist/types.d.ts +9 -59
  139. package/dist/types.d.ts.map +1 -1
  140. package/dist/ui.d.ts +3 -94
  141. package/dist/ui.d.ts.map +1 -1
  142. package/dist/ui.js +30 -1427
  143. package/dist/ui.js.map +1 -1
  144. package/dist/version.d.ts +1 -1
  145. package/dist/version.js +1 -1
  146. package/package.json +15 -8
  147. package/src/activity.ts +0 -9
  148. package/src/auth/bearer.ts +4 -33
  149. package/src/auth/clerk.ts +35 -48
  150. package/src/call-admission.ts +8 -6
  151. package/src/catalog-fingerprint.ts +52 -0
  152. package/src/catalog-limits.ts +11 -0
  153. package/src/catalog-service.ts +601 -0
  154. package/src/catalog.ts +57 -2
  155. package/src/concurrency.ts +39 -0
  156. package/src/connectors/api.ts +22 -12
  157. package/src/connectors/remote-mcp.ts +63 -45
  158. package/src/credentials.ts +2 -2
  159. package/src/errors.ts +21 -1
  160. package/src/execute.ts +193 -423
  161. package/src/executor-result.ts +3 -3
  162. package/src/executors/quickjs-child.ts +8 -4
  163. package/src/executors/quickjs-protocol.ts +1 -1
  164. package/src/executors/quickjs-runtime.ts +5 -1
  165. package/src/executors/quickjs.ts +11 -8
  166. package/src/favicon.ts +2 -2
  167. package/src/index.ts +70 -222
  168. package/src/invocation.ts +580 -0
  169. package/src/meta-tools.ts +393 -985
  170. package/src/operator-ui/browser.css +360 -0
  171. package/src/operator-ui/browser.ts +987 -0
  172. package/src/operator-ui/generated.ts +4 -0
  173. package/src/operator-ui/model.ts +105 -0
  174. package/src/registry.ts +459 -439
  175. package/src/retired-toolkits.ts +14 -0
  176. package/src/routes/activity.ts +215 -0
  177. package/src/routes/credentials.ts +254 -0
  178. package/src/routes/mcp.ts +339 -0
  179. package/src/routes/oauth.ts +322 -0
  180. package/src/routes/shared.ts +240 -0
  181. package/src/routes/ui.ts +143 -0
  182. package/src/server.ts +64 -1731
  183. package/src/skills.ts +15 -43
  184. package/src/storage/file.ts +3 -1
  185. package/src/storage/memory.ts +3 -1
  186. package/src/timeout.ts +2 -4
  187. package/src/types.ts +8 -60
  188. package/src/ui.ts +60 -1538
  189. package/src/version.ts +1 -1
  190. package/dist/credential-health.d.ts +0 -227
  191. package/dist/credential-health.d.ts.map +0 -1
  192. package/dist/credential-health.js +0 -606
  193. package/dist/credential-health.js.map +0 -1
  194. package/dist/toolkits.d.ts +0 -146
  195. package/dist/toolkits.d.ts.map +0 -1
  196. package/dist/toolkits.js +0 -322
  197. package/dist/toolkits.js.map +0 -1
  198. package/src/credential-health.ts +0 -832
  199. package/src/toolkits.ts +0 -461
@@ -0,0 +1,987 @@
1
+ import {
2
+ filterUiConnectors,
3
+ type CredentialManagementCapability,
4
+ type UiData,
5
+ } from "./model.js";
6
+
7
+ type OperatorPage = "connections" | "credentials" | "activity";
8
+
9
+ interface BrowserAuth {
10
+ kind: string;
11
+ publishableKey?: string;
12
+ frontendApiUrl?: string;
13
+ signInUrl?: string;
14
+ signUpUrl?: string;
15
+ }
16
+
17
+ interface BrowserClerkSession {
18
+ id?: string;
19
+ getToken(): Promise<string | null>;
20
+ }
21
+
22
+ interface BrowserClerk {
23
+ user?: unknown;
24
+ session?: BrowserClerkSession | null;
25
+ load(options: {
26
+ signInUrl?: string;
27
+ signUpUrl?: string;
28
+ signInFallbackRedirectUrl: string;
29
+ signUpFallbackRedirectUrl: string;
30
+ afterSignOutUrl: string;
31
+ }): Promise<void>;
32
+ addListener(
33
+ listener: (resources: { session?: BrowserClerkSession | null }) => void,
34
+ ): void;
35
+ redirectToSignIn(options: {
36
+ signInFallbackRedirectUrl: string;
37
+ signUpFallbackRedirectUrl: string;
38
+ }): void;
39
+ signOut(options: { redirectUrl: string }): Promise<unknown>;
40
+ }
41
+
42
+ interface UiActivityActor {
43
+ kind?: string;
44
+ id?: string;
45
+ namespace?: string;
46
+ label?: string;
47
+ }
48
+
49
+ interface UiActivityEvent {
50
+ occurredAt: string;
51
+ actor?: UiActivityActor;
52
+ connectorId: string;
53
+ toolName: string;
54
+ address: string;
55
+ source: string;
56
+ outcome: string;
57
+ durationMs: number;
58
+ attempts: number;
59
+ errorCode?: string;
60
+ }
61
+
62
+ interface UiActivityResponse {
63
+ events?: UiActivityEvent[];
64
+ nextCursor?: string;
65
+ error?: string;
66
+ }
67
+
68
+ interface UiActionResponse {
69
+ ok?: boolean;
70
+ message?: string;
71
+ error?: string;
72
+ }
73
+
74
+ interface OperatorElement extends HTMLElement {
75
+ disabled: boolean;
76
+ value: string;
77
+ }
78
+
79
+ declare const AUTH: BrowserAuth;
80
+ declare const MCP_URL: string;
81
+ declare const INITIAL_PAGE: OperatorPage;
82
+ declare const TITLE_SUFFIX: string;
83
+ declare const PRODUCT_NAME: string;
84
+ declare const PRODUCT_OPERATOR_LABEL: string;
85
+ declare const Clerk: BrowserClerk;
86
+
87
+ declare global {
88
+ interface Window {
89
+ Clerk?: BrowserClerk;
90
+ }
91
+ }
92
+
93
+ const KEY = "connecta:token";
94
+ const $ = (id: string): OperatorElement => {
95
+ const element = document.getElementById(id);
96
+ if (!element) throw new Error(`Missing operator UI element #${id}`);
97
+ return element as OperatorElement;
98
+ };
99
+ const PAGE_META = {
100
+ connections: { path: "/", label: "Connections" },
101
+ credentials: { path: "/credentials", label: "Credentials" },
102
+ activity: { path: "/activity", label: "Activity" },
103
+ } satisfies Record<OperatorPage, { path: string; label: string }>;
104
+ let DATA: UiData | null = null;
105
+ let ACTIVITY: UiActivityEvent[] = [];
106
+ let ACTIVITY_CURSOR: string | null = null;
107
+ let ACTIVITY_LOADED = false;
108
+ let CURRENT_PAGE: OperatorPage = INITIAL_PAGE;
109
+ let SESSION_GENERATION = 0;
110
+ let ACTIVITY_GENERATION = 0;
111
+ let CLERK_SESSION_ID: string | null = null;
112
+ $("mcpUrl").textContent = MCP_URL;
113
+
114
+ function errorMessage(error: unknown, fallback: string): string {
115
+ return error instanceof Error && error.message ? error.message : fallback;
116
+ }
117
+
118
+ function closestElement<T extends Element>(
119
+ target: EventTarget | null,
120
+ selector: string,
121
+ ): T | null {
122
+ const candidate = target as { closest?: (value: string) => Element | null } | null;
123
+ return typeof candidate?.closest === "function"
124
+ ? candidate.closest(selector) as T | null
125
+ : null;
126
+ }
127
+
128
+ function esc(s: unknown): string {
129
+ return String(s == null ? "" : s).replace(/[&<>"]/g, (c) =>
130
+ ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c] ?? c);
131
+ }
132
+
133
+ // Only http/https may become a clickable href — mirrors the server-side gate
134
+ // so a hostile scheme (javascript:, data:) can never be linked. Defense in depth.
135
+ function safeHttp(u: string): string | null {
136
+ try {
137
+ const p = new URL(u).protocol;
138
+ return p === "http:" || p === "https:" ? u : null;
139
+ } catch { return null; }
140
+ }
141
+
142
+ function formatDate(value?: string): string {
143
+ if (!value) return "";
144
+ const date = new Date(value);
145
+ return Number.isNaN(date.valueOf()) ? "" : date.toLocaleString();
146
+ }
147
+
148
+ function setNotice(message: string, isError = false): void {
149
+ $("credentialNotice").textContent = message || "";
150
+ $("credentialNotice").classList.toggle("error-notice", Boolean(isError));
151
+ $("credentialNotice").setAttribute("role", isError ? "alert" : "status");
152
+ }
153
+
154
+ function setOauthNotice(message: string, isError = false): void {
155
+ $("oauthNotice").textContent = message || "";
156
+ $("oauthNotice").classList.toggle("error-notice", Boolean(isError));
157
+ $("oauthNotice").setAttribute("role", isError ? "alert" : "status");
158
+ }
159
+
160
+ async function sessionToken(): Promise<string | null | undefined> {
161
+ return AUTH.kind === "clerk"
162
+ ? await Clerk.session?.getToken()
163
+ : localStorage.getItem(KEY);
164
+ }
165
+
166
+ function clearActivityState(): void {
167
+ ACTIVITY_GENERATION += 1;
168
+ ACTIVITY = [];
169
+ ACTIVITY_CURSOR = null;
170
+ ACTIVITY_LOADED = false;
171
+ $("activityList").innerHTML = "";
172
+ $("activityList").setAttribute("aria-busy", "false");
173
+ $("activityNotice").textContent = "";
174
+ $("activityNotice").setAttribute("role", "status");
175
+ $("activitySummary").textContent = "Arguments and results are never stored.";
176
+ $("activitySearch").value = "";
177
+ $("refreshActivity").disabled = false;
178
+ $("moreActivity").disabled = false;
179
+ $("moreActivity").classList.add("hidden");
180
+ }
181
+
182
+ function clearIdentityState(): void {
183
+ SESSION_GENERATION += 1;
184
+ DATA = null;
185
+ clearActivityState();
186
+ $("list").innerHTML = "";
187
+ setOauthNotice("");
188
+ $("filter").value = "";
189
+ $("credentialList").innerHTML = "";
190
+ $("credentialList").setAttribute("aria-busy", "false");
191
+ $("credentialNotice").textContent = "";
192
+ $("credentialNotice").setAttribute("role", "status");
193
+ $("credentialNotice").classList.remove("error-notice");
194
+ $("credentialUnavailable").textContent = "";
195
+ $("credentialUnavailable").classList.add("hidden");
196
+ $("credentialList").classList.add("hidden");
197
+ $("activityUnavailable").classList.add("hidden");
198
+ $("activityAvailable").classList.add("hidden");
199
+ $("credentialsNav").classList.add("hidden");
200
+ $("activityNav").classList.add("hidden");
201
+ $("serverInfo").textContent = PRODUCT_OPERATOR_LABEL;
202
+ }
203
+
204
+ function showGate(msg: string): void {
205
+ clearIdentityState();
206
+ $("app").classList.add("hidden");
207
+ $("appNav").classList.add("hidden");
208
+ $("gate").classList.remove("hidden");
209
+ $("err").textContent = msg || "";
210
+ if (AUTH.kind === "clerk") {
211
+ const signedIn = Boolean(window.Clerk && Clerk.user);
212
+ $("gateCopy").textContent = signedIn
213
+ ? "Signed in with Clerk, but this account cannot open deployment-wide operator pages."
214
+ : "Sign in with Clerk to open this operator page.";
215
+ $("signin").classList.toggle("hidden", signedIn);
216
+ $("gateSignout").classList.toggle("hidden", !signedIn);
217
+ } else {
218
+ $("gateCopy").textContent = "";
219
+ }
220
+ }
221
+
222
+ function pageForPath(path: string): OperatorPage {
223
+ if (path === "/credentials") return "credentials";
224
+ if (path === "/activity") return "activity";
225
+ return "connections";
226
+ }
227
+
228
+ function credentialUnavailableCopy(
229
+ capability?: CredentialManagementCapability,
230
+ ): string {
231
+ if (capability === "no_slots") {
232
+ return "No connectors declare operator-managed credential slots. Connector credentials remain configuration-as-code until a slot is declared.";
233
+ }
234
+ if (capability === "vault_not_configured") {
235
+ return "Credential storage is not configured. Set credentials.encryptionKey before managing connector credentials here.";
236
+ }
237
+ return "Credential management requires an eligible Clerk operator. Bearer-authenticated sessions can inspect connections but cannot manage stored credentials.";
238
+ }
239
+
240
+ function updateCapabilities(): void {
241
+ const credentialsAvailable =
242
+ DATA?.credentialManagement === "available";
243
+ $("credentialsNav").classList.toggle("hidden", !credentialsAvailable);
244
+ $("activityNav").classList.toggle("hidden", !DATA?.activityEnabled);
245
+ $("credentialUnavailable").classList.toggle("hidden", credentialsAvailable);
246
+ $("credentialList").classList.toggle("hidden", !credentialsAvailable);
247
+ $("credentialUnavailable").textContent = credentialsAvailable
248
+ ? ""
249
+ : credentialUnavailableCopy(DATA?.credentialManagement);
250
+ $("activityUnavailable").classList.toggle("hidden", Boolean(DATA?.activityEnabled));
251
+ $("activityAvailable").classList.toggle("hidden", !DATA?.activityEnabled);
252
+ }
253
+
254
+ function activatePage(
255
+ page: OperatorPage,
256
+ options?: { focus?: boolean },
257
+ ): void {
258
+ const next = PAGE_META[page] ? page : "connections";
259
+ CURRENT_PAGE = next;
260
+ for (const name of Object.keys(PAGE_META)) {
261
+ $(name + "View").classList.toggle("hidden", name !== next);
262
+ const link = $(name + "Nav");
263
+ if (name === next) link.setAttribute("aria-current", "page");
264
+ else link.removeAttribute("aria-current");
265
+ }
266
+ $("gateHeading").textContent = PAGE_META[next].label;
267
+ document.title = PAGE_META[next].label + " — " + TITLE_SUFFIX;
268
+ if (DATA) {
269
+ updateCapabilities();
270
+ if (next === "connections") renderConnections();
271
+ if (next === "credentials") renderCredentials();
272
+ if (next === "activity" && DATA.activityEnabled && !ACTIVITY_LOADED) {
273
+ loadActivity(true);
274
+ }
275
+ }
276
+ if (next !== "credentials") {
277
+ $("credentialList").innerHTML = "";
278
+ setNotice("");
279
+ }
280
+ // Focus what is actually on screen. While gated the page views are hidden, so
281
+ // focusing their heading is a silent no-op that drops focus to <body> and
282
+ // restarts the next Tab from the top of the document — the gate's own h1 is
283
+ // the visible heading, and activatePage has just relabelled it.
284
+ if (options?.focus) $(DATA ? next + "Heading" : "gateHeading").focus();
285
+ }
286
+
287
+ function navigateTo(page: OperatorPage, href: string): void {
288
+ history.pushState({ operatorPage: page }, "", href);
289
+ activatePage(page, { focus: true });
290
+ }
291
+
292
+ async function load(): Promise<void> {
293
+ const generation = SESSION_GENERATION;
294
+ let token;
295
+ try {
296
+ token = await sessionToken();
297
+ } catch (error) {
298
+ if (generation !== SESSION_GENERATION) return;
299
+ return showGate(
300
+ "Could not read the Clerk session: " +
301
+ errorMessage(error, "unknown error"),
302
+ );
303
+ }
304
+ if (generation !== SESSION_GENERATION) return;
305
+ if (!token) return showGate("");
306
+ let res;
307
+ try {
308
+ res = await fetch("/ui/data", { headers: { Authorization: "Bearer " + token } });
309
+ } catch (error) {
310
+ if (generation !== SESSION_GENERATION) return;
311
+ return showGate("Network error: " + errorMessage(error, "unknown error"));
312
+ }
313
+ if (generation !== SESSION_GENERATION) return;
314
+ if (res.status === 401 || res.status === 403) {
315
+ if (AUTH.kind === "clerk") {
316
+ return showGate(
317
+ res.status === 403
318
+ ? "This Clerk account is not allowed to access connecta."
319
+ : "Your Clerk session was not accepted. Sign out and try again."
320
+ );
321
+ }
322
+ localStorage.removeItem(KEY);
323
+ return showGate("Token rejected — enter a valid bearer token.");
324
+ }
325
+ if (!res.ok) return showGate("Error " + res.status);
326
+ let data: UiData;
327
+ try {
328
+ data = await res.json() as UiData;
329
+ } catch {
330
+ if (generation !== SESSION_GENERATION) return;
331
+ return showGate("Operator data could not be read.");
332
+ }
333
+ if (generation !== SESSION_GENERATION) return;
334
+ DATA = data;
335
+ $("gate").classList.add("hidden");
336
+ $("app").classList.remove("hidden");
337
+ $("appNav").classList.remove("hidden");
338
+ const si = data.serverInfo || {};
339
+ $("serverInfo").textContent =
340
+ (si.name || PRODUCT_NAME) + " v" + (data.connectaVersion || "?");
341
+ updateCapabilities();
342
+ activatePage(CURRENT_PAGE);
343
+ }
344
+
345
+ function actorLabel(actor?: UiActivityActor): string {
346
+ if (!actor || !actor.kind) return "unknown";
347
+ return actor.label
348
+ ? actor.kind + " · " + actor.label
349
+ : actor.id ? actor.kind + " · " + actor.id : actor.kind;
350
+ }
351
+
352
+ function renderActivity(): void {
353
+ const list = $("activityList");
354
+ list.innerHTML = "";
355
+ const query = $("activitySearch").value.trim().toLowerCase();
356
+ const visible = ACTIVITY.filter((event) => {
357
+ if (!query) return true;
358
+ const actor = event.actor || {};
359
+ return [
360
+ event.address,
361
+ event.connectorId,
362
+ event.toolName,
363
+ event.source,
364
+ event.outcome,
365
+ event.errorCode,
366
+ actor.kind,
367
+ actor.id,
368
+ actor.namespace,
369
+ actor.label,
370
+ ].some((value) => String(value || "").toLowerCase().includes(query));
371
+ });
372
+ const uniqueTools = new Set(ACTIVITY.map((event) => event.address)).size;
373
+ $("activitySummary").textContent = ACTIVITY.length
374
+ ? ACTIVITY.length + " loaded call" + (ACTIVITY.length === 1 ? "" : "s") +
375
+ " · " + uniqueTools + " tool" + (uniqueTools === 1 ? "" : "s") +
376
+ " · no arguments or results stored"
377
+ : "Arguments and results are never stored.";
378
+ if (visible.length === 0) {
379
+ list.innerHTML = '<div class="activity-empty">' +
380
+ (query ? "No loaded activity matches this search." :
381
+ "No connector tool calls recorded yet.") + "</div>";
382
+ }
383
+ for (const event of visible) {
384
+ const item = document.createElement("article");
385
+ const outcomeClass = [
386
+ "success",
387
+ "error",
388
+ "timeout",
389
+ "cancelled",
390
+ ].includes(event.outcome) ? event.outcome : "error";
391
+ item.className = "activity-item " + outcomeClass;
392
+ const retryCopy = event.attempts > 1
393
+ ? " · " + esc(event.attempts) + " attempts"
394
+ : "";
395
+ const errorCopy = event.errorCode ? " · " + esc(event.errorCode) : "";
396
+ const actorId = event.actor?.id
397
+ ? (event.actor.namespace
398
+ ? event.actor.namespace + " · " + event.actor.id
399
+ : event.actor.id)
400
+ : "";
401
+ const stableActorId = actorId &&
402
+ (event.actor?.label || event.actor?.namespace)
403
+ ? '<div class="activity-actor-id mono">' + esc(actorId) + "</div>"
404
+ : "";
405
+ item.innerHTML =
406
+ '<div class="activity-stamp"><span class="dot ' +
407
+ (outcomeClass === "success" ? "ok" : "") +
408
+ '" aria-hidden="true"></span><div><time class="activity-time" datetime="' +
409
+ esc(event.occurredAt) + '">' +
410
+ esc(formatDate(event.occurredAt)) +
411
+ '</time><div class="activity-actor">' + esc(actorLabel(event.actor)) +
412
+ "</div>" + stableActorId + "</div></div>" +
413
+ '<div><div class="activity-address">' + esc(event.address) +
414
+ '</div><div class="activity-detail">' + esc(event.source) + retryCopy +
415
+ errorCopy + '</div></div>' +
416
+ '<div><div class="activity-outcome">' + esc(event.outcome) +
417
+ '</div><div class="activity-detail">' + esc(event.durationMs) + ' ms</div></div>';
418
+ list.appendChild(item);
419
+ }
420
+ $("moreActivity").classList.toggle("hidden", !ACTIVITY_CURSOR);
421
+ }
422
+
423
+ async function loadActivity(reset: boolean): Promise<void> {
424
+ if (!DATA?.activityEnabled) return;
425
+ const sessionGeneration = SESSION_GENERATION;
426
+ if (reset) ACTIVITY_GENERATION += 1;
427
+ const activityGeneration = ACTIVITY_GENERATION;
428
+ const isCurrent = () =>
429
+ sessionGeneration === SESSION_GENERATION &&
430
+ activityGeneration === ACTIVITY_GENERATION;
431
+ $("activityNotice").textContent = "Loading activity…";
432
+ $("activityNotice").setAttribute("role", "status");
433
+ $("activityList").setAttribute("aria-busy", "true");
434
+ $("refreshActivity").disabled = true;
435
+ $("moreActivity").disabled = true;
436
+ try {
437
+ const token = await sessionToken();
438
+ if (!isCurrent()) return;
439
+ if (!token) return showGate("Your session has expired.");
440
+ const cursor = reset ? null : ACTIVITY_CURSOR;
441
+ const params = new URLSearchParams({ limit: "50" });
442
+ if (cursor) params.set("cursor", cursor);
443
+ const res = await fetch("/ui/activity?" + params, {
444
+ headers: { Authorization: "Bearer " + token },
445
+ });
446
+ if (!isCurrent()) return;
447
+ let payload: UiActivityResponse = {};
448
+ try {
449
+ payload = await res.json() as UiActivityResponse;
450
+ } catch {
451
+ // The status code below still owns the operator-facing error.
452
+ }
453
+ if (!isCurrent()) return;
454
+ if (res.status === 401) {
455
+ return showGate("Your session was not accepted. Sign in again.");
456
+ }
457
+ if (res.status === 403) {
458
+ clearActivityState();
459
+ $("activityNotice").setAttribute("role", "alert");
460
+ $("activityNotice").textContent =
461
+ "This identity may not read activity history.";
462
+ return;
463
+ }
464
+ if (!res.ok) {
465
+ throw new Error(
466
+ payload.error || "Activity could not be loaded (" + res.status + ")."
467
+ );
468
+ }
469
+ ACTIVITY = reset
470
+ ? (payload.events || [])
471
+ : ACTIVITY.concat(payload.events || []);
472
+ ACTIVITY_CURSOR = payload.nextCursor || null;
473
+ ACTIVITY_LOADED = true;
474
+ $("activityNotice").textContent = "";
475
+ renderActivity();
476
+ } catch (error) {
477
+ if (!isCurrent()) return;
478
+ $("activityNotice").setAttribute("role", "alert");
479
+ $("activityNotice").textContent = errorMessage(
480
+ error,
481
+ "Activity could not be loaded.",
482
+ );
483
+ } finally {
484
+ if (isCurrent()) {
485
+ $("activityList").setAttribute("aria-busy", "false");
486
+ $("refreshActivity").disabled = false;
487
+ $("moreActivity").disabled = false;
488
+ }
489
+ }
490
+ }
491
+
492
+ function renderConnections(): void {
493
+ const data = DATA;
494
+ if (!data) return;
495
+ const q = $("filter").value.trim().toLowerCase();
496
+ const list = $("list");
497
+ list.innerHTML = "";
498
+ for (const filtered of filterUiConnectors(data.connectors, q)) {
499
+ const c = filtered.connector;
500
+ const tools = filtered.tools;
501
+ const el = document.createElement("div");
502
+ el.className = "card";
503
+ const status = c.status === "ok"
504
+ ? "Connected"
505
+ : c.status === "auth_required"
506
+ ? "Authorization needed"
507
+ : "Unavailable";
508
+ let head = '<div class="connector-head"><div><div class="connector-title">' +
509
+ '<span class="dot ' + esc(c.status) + '" aria-hidden="true"></span>' +
510
+ '<h2>' + esc(c.title || c.id) + "</h2></div>";
511
+ if (c.description) {
512
+ head += '<p class="connector-description meta">' + esc(c.description) + "</p>";
513
+ }
514
+ head += '</div><div class="connector-state cap">' + esc(status) + " · " +
515
+ c.toolCount + (c.toolCount === 1 ? " tool" : " tools") +
516
+ '<br><span class="mono">' + esc(c.id) + "</span></div></div>";
517
+ if (c.message) {
518
+ head += '<p class="connector-message msg">' + esc(c.message) + "</p>";
519
+ }
520
+ if (c.authorizationUrl) {
521
+ const safe = safeHttp(c.authorizationUrl);
522
+ head += safe
523
+ ? '<p class="connector-auth"><a class="linklike" href="' + esc(safe) +
524
+ '" target="_blank" rel="noopener">Authorize connector →</a></p>'
525
+ : '<p class="connector-auth meta">Authorization URL: ' +
526
+ esc(c.authorizationUrl) + "</p>";
527
+ }
528
+ if (c.oauth && data.oauthManagement) {
529
+ const oauthName = c.title || c.id;
530
+ head += '<div class="credential-actions">';
531
+ head += '<button type="button" class="linklike danger" ' +
532
+ 'aria-label="Disconnect OAuth for ' + esc(oauthName) + '" ' +
533
+ 'data-oauth-action="disconnect" data-connector="' + esc(c.id) +
534
+ '">Disconnect OAuth</button>';
535
+ head += '<button type="button" class="linklike" ' +
536
+ 'aria-label="' + (c.status === "ok" ? "Reconnect OAuth for " :
537
+ "Restart authorization for ") + esc(oauthName) + '" ' +
538
+ 'data-oauth-action="reconnect" data-connector="' + esc(c.id) +
539
+ '">' + (c.status === "ok" ? "Reconnect OAuth" : "Restart authorization") +
540
+ "</button></div>";
541
+ }
542
+ if (c.credential) {
543
+ head += '<p class="connector-auth"><a class="linklike" href="/credentials" ' +
544
+ 'data-operator-page="credentials">Manage credential →</a></p>';
545
+ }
546
+ let body = "";
547
+ if (tools.length) {
548
+ body = "<details" + (q ? " open" : "") +
549
+ '><summary class="linklike">Show tools (' + tools.length + ")</summary>" +
550
+ '<div class="tool-list">';
551
+ for (const t of tools) {
552
+ body += '<div class="tool"><code>' + esc(t.address) + "</code>";
553
+ if (t.description) body += '<span class="td">' + esc(t.description) + "</span>";
554
+ body += "</div>";
555
+ }
556
+ body += "</div></details>";
557
+ }
558
+ el.innerHTML = head + body;
559
+ list.appendChild(el);
560
+ }
561
+ if (!list.children.length) {
562
+ list.innerHTML = '<p class="empty">' +
563
+ (q
564
+ ? "No connectors or tools match this filter."
565
+ : "No connectors are declared in this deployment.") +
566
+ "</p>";
567
+ }
568
+ }
569
+
570
+ async function operatorRequest(
571
+ path: string,
572
+ method: "DELETE" | "POST" | "PUT",
573
+ generation: number,
574
+ body?: object,
575
+ ): Promise<UiActionResponse | null> {
576
+ const token = await sessionToken();
577
+ if (generation !== SESSION_GENERATION) {
578
+ throw new Error("The operator session changed.");
579
+ }
580
+ if (!token) throw new Error("Your Clerk session has expired.");
581
+ const res = await fetch(path, {
582
+ method,
583
+ headers: {
584
+ Authorization: "Bearer " + token,
585
+ ...(body ? { "Content-Type": "application/json" } : {}),
586
+ },
587
+ ...(body ? { body: JSON.stringify(body) } : {}),
588
+ });
589
+ if (res.status === 204) return null;
590
+ let payload: UiActionResponse = {};
591
+ try {
592
+ payload = await res.json() as UiActionResponse;
593
+ } catch {
594
+ // The status code below still owns the operator-facing error.
595
+ }
596
+ if (!res.ok) throw new Error(payload.error || "Request failed (" + res.status + ").");
597
+ return payload;
598
+ }
599
+
600
+ $("list").onclick = async (event) => {
601
+ const button = closestElement<HTMLButtonElement>(
602
+ event.target,
603
+ "[data-oauth-action]",
604
+ );
605
+ if (!button) return;
606
+ const connector = button.dataset.connector;
607
+ const action = button.dataset.oauthAction;
608
+ if (!connector || (action !== "disconnect" && action !== "reconnect")) return;
609
+ const generation = SESSION_GENERATION;
610
+ const question = action === "disconnect"
611
+ ? "Disconnect OAuth for " + connector +
612
+ "? Stored credentials and any pending authorization will be removed."
613
+ : "Restart OAuth for " + connector +
614
+ "? Stored credentials and any pending authorization will be replaced.";
615
+ if (!window.confirm(question)) return;
616
+
617
+ setOauthNotice("");
618
+ const buttons = [...document.querySelectorAll<HTMLButtonElement>(
619
+ '[data-connector="' + CSS.escape(connector) + '"]'
620
+ )];
621
+ buttons.forEach((item) => { item.disabled = true; });
622
+ try {
623
+ const result = await operatorRequest(
624
+ "/ui/oauth/" + encodeURIComponent(connector),
625
+ action === "disconnect" ? "DELETE" : "POST",
626
+ generation,
627
+ );
628
+ if (generation !== SESSION_GENERATION) return;
629
+ await load();
630
+ if (generation !== SESSION_GENERATION) return;
631
+ setOauthNotice(
632
+ action === "disconnect"
633
+ ? "OAuth disconnected. Restart authorization when you are ready to reconnect."
634
+ : result?.message ||
635
+ "Authorization restarted. Open the authorization link to reconnect.",
636
+ );
637
+ $("oauthNotice").focus();
638
+ } catch (error) {
639
+ if (generation !== SESSION_GENERATION) return;
640
+ // A reset can publish its durable epoch before best-effort physical cleanup
641
+ // reports failure. Refresh so stale tools/health/actions never survive that
642
+ // partial success, while preserving the original error for the operator.
643
+ try {
644
+ await load();
645
+ } catch {
646
+ // The mutation error is the actionable one; load() already owns its gate.
647
+ }
648
+ if (generation !== SESSION_GENERATION) return;
649
+ setOauthNotice(errorMessage(error, "OAuth action failed."), true);
650
+ $("oauthNotice").focus();
651
+ } finally {
652
+ if (generation === SESSION_GENERATION) {
653
+ buttons.forEach((item) => { item.disabled = false; });
654
+ }
655
+ }
656
+ };
657
+
658
+ function renderCredentials(): void {
659
+ const data = DATA;
660
+ if (!data) return;
661
+ const list = $("credentialList");
662
+ list.innerHTML = "";
663
+ if (data.credentialManagement !== "available") return;
664
+ for (const c of data.connectors) {
665
+ const cred = c.credential;
666
+ if (!cred) continue;
667
+ const configured = Boolean(cred.configured);
668
+ const removable = configured || Boolean(cred.removable);
669
+ const state = configured
670
+ ? cred.fields?.length
671
+ ? "configured"
672
+ : "configured · ••••" + esc(cred.lastFour || "")
673
+ : "not configured";
674
+ const updated = configured && cred.updatedAt
675
+ ? " · updated " + esc(formatDate(cred.updatedAt))
676
+ : "";
677
+ const el = document.createElement("section");
678
+ el.className = "credential-card";
679
+ el.id = "credential-" + c.id;
680
+ el.setAttribute("aria-labelledby", "credential-title-" + c.id);
681
+ let body = '<div class="credential-head"><div class="connector-title">' +
682
+ '<span class="dot ' + (configured ? "ok" : "auth_required") +
683
+ '" aria-hidden="true"></span><h2 id="credential-title-' + esc(c.id) + '">' +
684
+ esc(c.title || c.id) + '</h2></div><span class="credential-state">' +
685
+ state + updated + "</span></div>";
686
+ body += '<p class="mono">' + esc(c.id) + " · " + esc(cred.label) + "</p>";
687
+ if (cred.description) {
688
+ body += '<p class="credential-copy meta">' + esc(cred.description) + "</p>";
689
+ }
690
+ if (cred.fields?.length) {
691
+ body += '<div class="credential-field-summary">';
692
+ for (const field of cred.fields) {
693
+ const fieldState = field.configured
694
+ ? "configured · ••••" + esc(field.lastFour || "") +
695
+ (field.updatedAt ? " · updated " + esc(formatDate(field.updatedAt)) : "")
696
+ : "not configured";
697
+ body += '<div><span>' + esc(field.label) +
698
+ '</span><span class="meta">' + fieldState + "</span></div>";
699
+ }
700
+ body += "</div>";
701
+ }
702
+ if (cred.error) body += '<div class="msg">' + esc(cred.error) + "</div>";
703
+ // Leftover stored fields are not an error — the credential still works, so
704
+ // this stays muted copy rather than the msg block an actual failure earns.
705
+ if (cred.notice) {
706
+ body += '<p class="credential-copy meta">' + esc(cred.notice) + "</p>";
707
+ }
708
+ body += '<div class="credential-actions">';
709
+ body += '<button class="linklike" type="button" data-credential-action="edit" data-connector="' +
710
+ esc(c.id) + '">' + (removable ? "Replace" : "Add credential") + "</button>";
711
+ if (configured && cred.testable) {
712
+ body += '<button class="linklike" type="button" data-credential-action="test" data-connector="' +
713
+ esc(c.id) + '">Test</button>';
714
+ }
715
+ if (removable) {
716
+ body += '<button type="button" class="linklike danger" data-credential-action="remove" data-connector="' +
717
+ esc(c.id) + '">Remove</button>';
718
+ }
719
+ body += "</div>";
720
+ body += '<div class="credential-form hidden" data-credential-form="' +
721
+ esc(c.id) + '">';
722
+ if (cred.fields && cred.fields.length) {
723
+ body += '<div class="credential-fields">';
724
+ for (const [index, field] of cred.fields.entries()) {
725
+ const inputId = "credential-input-" + c.id + "-" + index;
726
+ body += '<div class="credential-field"><label for="' + esc(inputId) + '">' +
727
+ esc(field.label) + '</label><input id="' + esc(inputId) + '" type="' +
728
+ esc(field.inputType || "password") + '" data-credential-field="' +
729
+ esc(field.name) + '" placeholder="' + esc(field.placeholder || field.label) +
730
+ '" autocomplete="' +
731
+ (field.inputType === "password" ? "new-password" : "off") +
732
+ '" autocapitalize="none" spellcheck="false"></div>';
733
+ }
734
+ body += "</div>";
735
+ } else {
736
+ const inputId = "credential-input-" + c.id;
737
+ body += '<label class="visually-hidden" for="' + esc(inputId) + '">' +
738
+ esc(cred.label) + '</label><input id="' + esc(inputId) +
739
+ '" type="password" data-credential-input="' +
740
+ esc(c.id) + '" aria-label="' + esc(cred.label) + '" placeholder="' +
741
+ esc(cred.placeholder || "Paste credential") +
742
+ '" autocomplete="new-password" autocapitalize="none" spellcheck="false">';
743
+ }
744
+ body += '<button class="linklike" type="button" data-credential-action="save" data-connector="' +
745
+ esc(c.id) + '">Save</button><button class="linklike" type="button" data-credential-action="cancel" data-connector="' +
746
+ esc(c.id) + '">Cancel</button></div>';
747
+ el.innerHTML = body;
748
+ list.appendChild(el);
749
+ }
750
+ }
751
+
752
+ async function credentialRequest(
753
+ connector: string,
754
+ method: "DELETE" | "POST" | "PUT",
755
+ action: "" | "test",
756
+ body: { value: string } | { values: Record<string, string> } | null,
757
+ generation: number,
758
+ ): Promise<UiActionResponse | null> {
759
+ const suffix = action ? "/" + action : "";
760
+ return operatorRequest(
761
+ "/ui/credentials/" + encodeURIComponent(connector) + suffix,
762
+ method,
763
+ generation,
764
+ body ?? undefined,
765
+ );
766
+ }
767
+
768
+ function credentialForm(connector: string): HTMLElement {
769
+ const form = document.querySelector<HTMLElement>(
770
+ '[data-credential-form="' + CSS.escape(connector) + '"]',
771
+ );
772
+ if (!form) throw new Error("Credential form is unavailable.");
773
+ return form;
774
+ }
775
+
776
+ $("credentialList").onclick = async (event) => {
777
+ const button = closestElement<HTMLButtonElement>(
778
+ event.target,
779
+ "[data-credential-action]",
780
+ );
781
+ if (!button) return;
782
+ const connector = button.dataset.connector;
783
+ const action = button.dataset.credentialAction;
784
+ if (!connector || !action) return;
785
+ const form = credentialForm(connector);
786
+ const generation = SESSION_GENERATION;
787
+
788
+ if (action === "edit") {
789
+ form.classList.remove("hidden");
790
+ form.querySelector("input")?.focus();
791
+ return;
792
+ }
793
+ if (action === "cancel") {
794
+ form.querySelectorAll("input").forEach((input) => { input.value = ""; });
795
+ form.classList.add("hidden");
796
+ document.querySelector<HTMLButtonElement>(
797
+ '[data-credential-action="edit"][data-connector="' +
798
+ CSS.escape(connector) + '"]'
799
+ )?.focus();
800
+ return;
801
+ }
802
+ if (action === "remove" && !window.confirm(
803
+ "Remove this credential? The connector will stop authenticating until a replacement is added."
804
+ )) return;
805
+
806
+ setNotice("");
807
+ $("credentialList").setAttribute("aria-busy", "true");
808
+ const buttons = [...document.querySelectorAll<HTMLButtonElement>(
809
+ '[data-connector="' + CSS.escape(connector) + '"]'
810
+ )];
811
+ buttons.forEach((item) => { item.disabled = true; });
812
+ try {
813
+ if (action === "save") {
814
+ const fieldInputs = [
815
+ ...form.querySelectorAll<HTMLInputElement>("[data-credential-field]"),
816
+ ];
817
+ if (fieldInputs.length) {
818
+ const values: Record<string, string> = {};
819
+ for (const input of fieldInputs) {
820
+ const value = input.value.trim();
821
+ if (!value) throw new Error("Complete every credential field before saving.");
822
+ const field = input.dataset.credentialField;
823
+ if (!field) throw new Error("Credential field is unnamed.");
824
+ values[field] = value;
825
+ }
826
+ await credentialRequest(connector, "PUT", "", { values }, generation);
827
+ } else {
828
+ const input = form.querySelector<HTMLInputElement>(
829
+ "[data-credential-input]",
830
+ );
831
+ if (!input) throw new Error("Credential input is unavailable.");
832
+ const value = input.value.trim();
833
+ if (!value) throw new Error("Paste a credential before saving.");
834
+ await credentialRequest(connector, "PUT", "", { value }, generation);
835
+ }
836
+ if (generation !== SESSION_GENERATION) return;
837
+ form.querySelectorAll<HTMLInputElement>("input")
838
+ .forEach((input) => { input.value = ""; });
839
+ setNotice("Credential saved.");
840
+ await load();
841
+ if (generation !== SESSION_GENERATION) return;
842
+ $("credentialNotice").focus();
843
+ } else if (action === "remove") {
844
+ await credentialRequest(connector, "DELETE", "", null, generation);
845
+ if (generation !== SESSION_GENERATION) return;
846
+ setNotice("Credential removed.");
847
+ await load();
848
+ if (generation !== SESSION_GENERATION) return;
849
+ $("credentialNotice").focus();
850
+ } else if (action === "test") {
851
+ const result = await credentialRequest(
852
+ connector,
853
+ "POST",
854
+ "test",
855
+ null,
856
+ generation,
857
+ );
858
+ if (generation !== SESSION_GENERATION) return;
859
+ setNotice(
860
+ result?.message ||
861
+ (result?.ok ? "Credential is valid." : "Credential test failed."),
862
+ !result?.ok,
863
+ );
864
+ }
865
+ } catch (error) {
866
+ if (generation !== SESSION_GENERATION) return;
867
+ setNotice(errorMessage(error, "Credential action failed."), true);
868
+ } finally {
869
+ if (generation === SESSION_GENERATION) {
870
+ $("credentialList").setAttribute("aria-busy", "false");
871
+ buttons.forEach((item) => { item.disabled = false; });
872
+ }
873
+ }
874
+ };
875
+
876
+ $("save").onclick = async () => {
877
+ const v = $("token").value.trim();
878
+ if (!v) return;
879
+ clearIdentityState();
880
+ localStorage.setItem(KEY, v);
881
+ $("token").value = "";
882
+ await load();
883
+ if (DATA) $(CURRENT_PAGE + "Heading").focus();
884
+ };
885
+ $("change").onclick = () => {
886
+ localStorage.removeItem(KEY);
887
+ showGate("");
888
+ $("token").focus();
889
+ };
890
+ $("copyMcpUrl").onclick = async () => {
891
+ const button = $("copyMcpUrl");
892
+ try {
893
+ await navigator.clipboard.writeText(MCP_URL);
894
+ button.textContent = "Copied";
895
+ } catch {
896
+ button.textContent = "Copy failed";
897
+ }
898
+ window.setTimeout(() => { button.textContent = "Copy URL"; }, 1600);
899
+ };
900
+ $("signin").onclick = () => Clerk.redirectToSignIn({
901
+ signInFallbackRedirectUrl: window.location.href,
902
+ signUpFallbackRedirectUrl: window.location.href,
903
+ });
904
+ function signOut(): Promise<unknown> {
905
+ clearIdentityState();
906
+ return Clerk.signOut({ redirectUrl: window.location.href });
907
+ }
908
+ $("gateSignout").onclick = signOut;
909
+ $("signout").onclick = signOut;
910
+ $("filter").oninput = () => { if (DATA) renderConnections(); };
911
+ $("refreshActivity").onclick = () => loadActivity(true);
912
+ $("moreActivity").onclick = () => loadActivity(false);
913
+ $("activitySearch").oninput = () => renderActivity();
914
+ document.addEventListener("click", (event) => {
915
+ const link = closestElement<HTMLAnchorElement>(
916
+ event.target,
917
+ "a[data-operator-page]",
918
+ );
919
+ if (
920
+ !link ||
921
+ event.defaultPrevented ||
922
+ event.button !== 0 ||
923
+ event.metaKey ||
924
+ event.ctrlKey ||
925
+ event.shiftKey ||
926
+ event.altKey
927
+ ) return;
928
+ const target = new URL(link.href, window.location.href);
929
+ if (target.origin !== window.location.origin) return;
930
+ const page = link.dataset.operatorPage;
931
+ if (
932
+ page !== "connections" &&
933
+ page !== "credentials" &&
934
+ page !== "activity"
935
+ ) return;
936
+ event.preventDefault();
937
+ navigateTo(page, target.pathname + target.search + target.hash);
938
+ });
939
+ window.addEventListener("popstate", () => {
940
+ activatePage(pageForPath(window.location.pathname), { focus: true });
941
+ });
942
+
943
+ async function init(): Promise<void> {
944
+ if (AUTH.kind === "clerk") {
945
+ $("clerkGate").classList.remove("hidden");
946
+ $("signout").classList.remove("hidden");
947
+ try {
948
+ if (!window.Clerk) {
949
+ return showGate("Clerk could not load. Check your network and try again.");
950
+ }
951
+ await Clerk.load({
952
+ ...(AUTH.signInUrl ? { signInUrl: AUTH.signInUrl } : {}),
953
+ ...(AUTH.signUpUrl ? { signUpUrl: AUTH.signUpUrl } : {}),
954
+ signInFallbackRedirectUrl: window.location.href,
955
+ signUpFallbackRedirectUrl: window.location.href,
956
+ afterSignOutUrl: window.location.href,
957
+ });
958
+ CLERK_SESSION_ID = Clerk.session?.id ?? null;
959
+ Clerk.addListener((resources) => {
960
+ const nextSessionId = resources.session?.id ?? null;
961
+ if (nextSessionId === CLERK_SESSION_ID) return;
962
+ CLERK_SESSION_ID = nextSessionId;
963
+ // Clerk has already updated its public session before notifying
964
+ // listeners. Clear synchronously so stale identity-scoped data cannot
965
+ // be repainted while the replacement identity is being fetched.
966
+ showGate("");
967
+ void load();
968
+ });
969
+ } catch (error) {
970
+ return showGate(
971
+ "Clerk could not initialize: " +
972
+ errorMessage(error, "unknown error"),
973
+ );
974
+ }
975
+ } else {
976
+ $("tokenGate").classList.remove("hidden");
977
+ $("change").classList.remove("hidden");
978
+ }
979
+ activatePage(pageForPath(window.location.pathname));
980
+ await load();
981
+ }
982
+
983
+ if (AUTH.kind === "clerk") {
984
+ window.addEventListener("load", init);
985
+ } else {
986
+ init();
987
+ }