@zackbart/connecta 0.15.1 → 0.16.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 (335) hide show
  1. package/AGENTS.md +40 -20
  2. package/CHANGELOG.md +653 -0
  3. package/README.md +25 -5
  4. package/bin/connecta.mjs +44 -3
  5. package/dist/access-tokens.d.ts +0 -1
  6. package/dist/access-tokens.js +0 -1
  7. package/dist/activity.d.ts +50 -2
  8. package/dist/activity.js +34 -1
  9. package/dist/apps-shell.d.ts +0 -1
  10. package/dist/apps-shell.js +0 -1
  11. package/dist/auth/bearer.d.ts +0 -1
  12. package/dist/auth/bearer.js +0 -1
  13. package/dist/auth/clerk.d.ts +0 -1
  14. package/dist/auth/clerk.js +45 -9
  15. package/dist/auth/downstream-oauth.d.ts +0 -1
  16. package/dist/auth/downstream-oauth.js +0 -1
  17. package/dist/call-admission.d.ts +0 -1
  18. package/dist/call-admission.js +0 -1
  19. package/dist/catalog-drift.d.ts +99 -0
  20. package/dist/catalog-drift.js +251 -0
  21. package/dist/catalog-fingerprint.d.ts +0 -1
  22. package/dist/catalog-fingerprint.js +0 -1
  23. package/dist/catalog-limits.d.ts +0 -1
  24. package/dist/catalog-limits.js +0 -1
  25. package/dist/catalog-service.d.ts +0 -1
  26. package/dist/catalog-service.js +41 -5
  27. package/dist/catalog.d.ts +11 -1
  28. package/dist/catalog.js +20 -1
  29. package/dist/concurrency.d.ts +0 -1
  30. package/dist/concurrency.js +0 -1
  31. package/dist/connector-scope.d.ts +0 -1
  32. package/dist/connector-scope.js +0 -1
  33. package/dist/connectors/api.d.ts +29 -21
  34. package/dist/connectors/api.js +45 -19
  35. package/dist/connectors/guarded-fetch.d.ts +106 -0
  36. package/dist/connectors/guarded-fetch.js +289 -0
  37. package/dist/connectors/remote-mcp.d.ts +0 -1
  38. package/dist/connectors/remote-mcp.js +0 -1
  39. package/dist/credentials.d.ts +0 -1
  40. package/dist/credentials.js +0 -1
  41. package/dist/errors.d.ts +30 -3
  42. package/dist/errors.js +1 -1
  43. package/dist/execute.d.ts +0 -1
  44. package/dist/execute.js +0 -1
  45. package/dist/executor-admission.d.ts +11 -1
  46. package/dist/executor-admission.js +28 -1
  47. package/dist/executor-result.d.ts +0 -1
  48. package/dist/executor-result.js +0 -1
  49. package/dist/executors/quickjs-child.d.ts +0 -1
  50. package/dist/executors/quickjs-child.js +0 -1
  51. package/dist/executors/quickjs-protocol.d.ts +0 -1
  52. package/dist/executors/quickjs-protocol.js +0 -1
  53. package/dist/executors/quickjs-runtime.d.ts +0 -1
  54. package/dist/executors/quickjs-runtime.js +0 -1
  55. package/dist/executors/quickjs.d.ts +0 -1
  56. package/dist/executors/quickjs.js +2 -1
  57. package/dist/favicon.d.ts +0 -1
  58. package/dist/favicon.js +0 -1
  59. package/dist/index.d.ts +2 -3
  60. package/dist/index.js +24 -7
  61. package/dist/invocation.d.ts +0 -1
  62. package/dist/invocation.js +0 -1
  63. package/dist/json-schema.d.ts +0 -1
  64. package/dist/json-schema.js +0 -1
  65. package/dist/mcp-result.d.ts +0 -1
  66. package/dist/mcp-result.js +0 -1
  67. package/dist/meta-tools.d.ts +0 -1
  68. package/dist/meta-tools.js +0 -1
  69. package/dist/node.d.ts +0 -1
  70. package/dist/node.js +0 -1
  71. package/dist/operator-ui/generated.d.ts +2 -3
  72. package/dist/operator-ui/generated.js +3 -4
  73. package/dist/operator-ui/model.d.ts +10 -1
  74. package/dist/operator-ui/model.js +0 -1
  75. package/dist/operator-ui/view.d.ts +167 -0
  76. package/dist/operator-ui/view.js +270 -0
  77. package/dist/providers/cloudflare.d.ts +0 -1
  78. package/dist/providers/cloudflare.js +261 -299
  79. package/dist/providers/linear.d.ts +26 -5
  80. package/dist/providers/linear.js +32 -61
  81. package/dist/providers/mixpanel.d.ts +35 -4
  82. package/dist/providers/mixpanel.js +53 -79
  83. package/dist/providers/notion.d.ts +0 -1
  84. package/dist/providers/notion.js +87 -42
  85. package/dist/providers/stripe.d.ts +13 -1
  86. package/dist/providers/stripe.js +31 -60
  87. package/dist/registry.d.ts +34 -2
  88. package/dist/registry.js +85 -5
  89. package/dist/retired-toolkits.d.ts +0 -1
  90. package/dist/retired-toolkits.js +0 -1
  91. package/dist/routes/access-tokens.d.ts +0 -1
  92. package/dist/routes/access-tokens.js +0 -1
  93. package/dist/routes/activity.d.ts +0 -1
  94. package/dist/routes/activity.js +0 -1
  95. package/dist/routes/credentials.d.ts +0 -1
  96. package/dist/routes/credentials.js +0 -1
  97. package/dist/routes/mcp.d.ts +0 -1
  98. package/dist/routes/mcp.js +0 -1
  99. package/dist/routes/oauth.d.ts +0 -1
  100. package/dist/routes/oauth.js +0 -1
  101. package/dist/routes/shared.d.ts +2 -1
  102. package/dist/routes/shared.js +0 -1
  103. package/dist/routes/ui.d.ts +0 -1
  104. package/dist/routes/ui.js +0 -1
  105. package/dist/server.d.ts +0 -1
  106. package/dist/server.js +11 -1
  107. package/dist/skills.d.ts +0 -1
  108. package/dist/skills.js +0 -1
  109. package/dist/storage/file.d.ts +0 -1
  110. package/dist/storage/file.js +0 -1
  111. package/dist/storage/memory.d.ts +0 -1
  112. package/dist/storage/memory.js +0 -1
  113. package/dist/timeout.d.ts +0 -1
  114. package/dist/timeout.js +0 -1
  115. package/dist/tool-safety.d.ts +0 -1
  116. package/dist/tool-safety.js +0 -1
  117. package/dist/types.d.ts +45 -1
  118. package/dist/types.js +0 -1
  119. package/dist/ui.d.ts +0 -1
  120. package/dist/ui.js +13 -137
  121. package/dist/validate.d.ts +13 -15
  122. package/dist/validate.js +24 -11
  123. package/dist/version.d.ts +1 -2
  124. package/dist/version.js +1 -2
  125. package/documentation/architecture.md +186 -5
  126. package/documentation/auth.md +11 -0
  127. package/documentation/call-admission.md +142 -6
  128. package/documentation/cloudflare.md +92 -10
  129. package/documentation/code-first-exploration.md +2 -2
  130. package/documentation/code-mode.md +14 -14
  131. package/documentation/connector-guides.md +172 -5
  132. package/documentation/connectors.md +89 -0
  133. package/documentation/linear.md +20 -2
  134. package/documentation/mcp-ui-design.md +2 -2
  135. package/documentation/meta-tools.md +7 -1
  136. package/documentation/mixpanel.md +42 -9
  137. package/documentation/notion.md +23 -2
  138. package/documentation/operations.md +325 -5
  139. package/documentation/operator-ui.md +120 -5
  140. package/documentation/provider-audit.md +160 -0
  141. package/documentation/provider-conventions.md +667 -0
  142. package/documentation/request-admission.md +167 -5
  143. package/documentation/rich-output-design.md +1 -2
  144. package/documentation/stripe.md +15 -0
  145. package/documentation/upgrading.md +454 -0
  146. package/ethos.md +14 -9
  147. package/examples/worker/README.md +102 -12
  148. package/examples/worker/src/index.ts +46 -1
  149. package/examples/worker/wrangler.jsonc +14 -1
  150. package/package.json +10 -6
  151. package/templates/node/.dockerignore +10 -0
  152. package/templates/node/.env.example +40 -3
  153. package/templates/node/AGENTS.md +16 -1
  154. package/templates/node/Dockerfile +49 -0
  155. package/templates/node/README.md +117 -2
  156. package/templates/node/docker-compose.yml +39 -0
  157. package/templates/node/package.json +1 -1
  158. package/templates/node/src/file-activity.ts +182 -0
  159. package/templates/node/src/index.ts +80 -4
  160. package/assets/connecta-clay-hero.png +0 -0
  161. package/dist/access-tokens.d.ts.map +0 -1
  162. package/dist/access-tokens.js.map +0 -1
  163. package/dist/activity.d.ts.map +0 -1
  164. package/dist/activity.js.map +0 -1
  165. package/dist/apps-shell.d.ts.map +0 -1
  166. package/dist/apps-shell.js.map +0 -1
  167. package/dist/auth/bearer.d.ts.map +0 -1
  168. package/dist/auth/bearer.js.map +0 -1
  169. package/dist/auth/clerk.d.ts.map +0 -1
  170. package/dist/auth/clerk.js.map +0 -1
  171. package/dist/auth/downstream-oauth.d.ts.map +0 -1
  172. package/dist/auth/downstream-oauth.js.map +0 -1
  173. package/dist/call-admission.d.ts.map +0 -1
  174. package/dist/call-admission.js.map +0 -1
  175. package/dist/catalog-fingerprint.d.ts.map +0 -1
  176. package/dist/catalog-fingerprint.js.map +0 -1
  177. package/dist/catalog-limits.d.ts.map +0 -1
  178. package/dist/catalog-limits.js.map +0 -1
  179. package/dist/catalog-service.d.ts.map +0 -1
  180. package/dist/catalog-service.js.map +0 -1
  181. package/dist/catalog.d.ts.map +0 -1
  182. package/dist/catalog.js.map +0 -1
  183. package/dist/concurrency.d.ts.map +0 -1
  184. package/dist/concurrency.js.map +0 -1
  185. package/dist/connector-scope.d.ts.map +0 -1
  186. package/dist/connector-scope.js.map +0 -1
  187. package/dist/connectors/api.d.ts.map +0 -1
  188. package/dist/connectors/api.js.map +0 -1
  189. package/dist/connectors/remote-mcp.d.ts.map +0 -1
  190. package/dist/connectors/remote-mcp.js.map +0 -1
  191. package/dist/credentials.d.ts.map +0 -1
  192. package/dist/credentials.js.map +0 -1
  193. package/dist/errors.d.ts.map +0 -1
  194. package/dist/errors.js.map +0 -1
  195. package/dist/execute.d.ts.map +0 -1
  196. package/dist/execute.js.map +0 -1
  197. package/dist/executor-admission.d.ts.map +0 -1
  198. package/dist/executor-admission.js.map +0 -1
  199. package/dist/executor-result.d.ts.map +0 -1
  200. package/dist/executor-result.js.map +0 -1
  201. package/dist/executors/quickjs-child.d.ts.map +0 -1
  202. package/dist/executors/quickjs-child.js.map +0 -1
  203. package/dist/executors/quickjs-protocol.d.ts.map +0 -1
  204. package/dist/executors/quickjs-protocol.js.map +0 -1
  205. package/dist/executors/quickjs-runtime.d.ts.map +0 -1
  206. package/dist/executors/quickjs-runtime.js.map +0 -1
  207. package/dist/executors/quickjs.d.ts.map +0 -1
  208. package/dist/executors/quickjs.js.map +0 -1
  209. package/dist/favicon.d.ts.map +0 -1
  210. package/dist/favicon.js.map +0 -1
  211. package/dist/index.d.ts.map +0 -1
  212. package/dist/index.js.map +0 -1
  213. package/dist/invocation.d.ts.map +0 -1
  214. package/dist/invocation.js.map +0 -1
  215. package/dist/json-schema.d.ts.map +0 -1
  216. package/dist/json-schema.js.map +0 -1
  217. package/dist/mcp-result.d.ts.map +0 -1
  218. package/dist/mcp-result.js.map +0 -1
  219. package/dist/meta-tools.d.ts.map +0 -1
  220. package/dist/meta-tools.js.map +0 -1
  221. package/dist/node.d.ts.map +0 -1
  222. package/dist/node.js.map +0 -1
  223. package/dist/operator-ui/generated.d.ts.map +0 -1
  224. package/dist/operator-ui/generated.js.map +0 -1
  225. package/dist/operator-ui/model.d.ts.map +0 -1
  226. package/dist/operator-ui/model.js.map +0 -1
  227. package/dist/providers/cloudflare.d.ts.map +0 -1
  228. package/dist/providers/cloudflare.js.map +0 -1
  229. package/dist/providers/linear.d.ts.map +0 -1
  230. package/dist/providers/linear.js.map +0 -1
  231. package/dist/providers/mixpanel.d.ts.map +0 -1
  232. package/dist/providers/mixpanel.js.map +0 -1
  233. package/dist/providers/notion.d.ts.map +0 -1
  234. package/dist/providers/notion.js.map +0 -1
  235. package/dist/providers/stripe.d.ts.map +0 -1
  236. package/dist/providers/stripe.js.map +0 -1
  237. package/dist/registry.d.ts.map +0 -1
  238. package/dist/registry.js.map +0 -1
  239. package/dist/retired-toolkits.d.ts.map +0 -1
  240. package/dist/retired-toolkits.js.map +0 -1
  241. package/dist/routes/access-tokens.d.ts.map +0 -1
  242. package/dist/routes/access-tokens.js.map +0 -1
  243. package/dist/routes/activity.d.ts.map +0 -1
  244. package/dist/routes/activity.js.map +0 -1
  245. package/dist/routes/credentials.d.ts.map +0 -1
  246. package/dist/routes/credentials.js.map +0 -1
  247. package/dist/routes/mcp.d.ts.map +0 -1
  248. package/dist/routes/mcp.js.map +0 -1
  249. package/dist/routes/oauth.d.ts.map +0 -1
  250. package/dist/routes/oauth.js.map +0 -1
  251. package/dist/routes/shared.d.ts.map +0 -1
  252. package/dist/routes/shared.js.map +0 -1
  253. package/dist/routes/ui.d.ts.map +0 -1
  254. package/dist/routes/ui.js.map +0 -1
  255. package/dist/server.d.ts.map +0 -1
  256. package/dist/server.js.map +0 -1
  257. package/dist/skills.d.ts.map +0 -1
  258. package/dist/skills.js.map +0 -1
  259. package/dist/storage/file.d.ts.map +0 -1
  260. package/dist/storage/file.js.map +0 -1
  261. package/dist/storage/memory.d.ts.map +0 -1
  262. package/dist/storage/memory.js.map +0 -1
  263. package/dist/timeout.d.ts.map +0 -1
  264. package/dist/timeout.js.map +0 -1
  265. package/dist/tool-safety.d.ts.map +0 -1
  266. package/dist/tool-safety.js.map +0 -1
  267. package/dist/types.d.ts.map +0 -1
  268. package/dist/types.js.map +0 -1
  269. package/dist/ui.d.ts.map +0 -1
  270. package/dist/ui.js.map +0 -1
  271. package/dist/validate.d.ts.map +0 -1
  272. package/dist/validate.js.map +0 -1
  273. package/dist/version.d.ts.map +0 -1
  274. package/dist/version.js.map +0 -1
  275. package/examples/node/README.md +0 -52
  276. package/examples/node/src/index.ts +0 -71
  277. package/src/access-tokens.ts +0 -289
  278. package/src/activity.ts +0 -234
  279. package/src/apps-shell.ts +0 -370
  280. package/src/auth/bearer.ts +0 -61
  281. package/src/auth/clerk.ts +0 -553
  282. package/src/auth/downstream-oauth.ts +0 -575
  283. package/src/call-admission.ts +0 -521
  284. package/src/catalog-fingerprint.ts +0 -52
  285. package/src/catalog-limits.ts +0 -11
  286. package/src/catalog-service.ts +0 -1153
  287. package/src/catalog.ts +0 -790
  288. package/src/concurrency.ts +0 -39
  289. package/src/connector-scope.ts +0 -66
  290. package/src/connectors/api.ts +0 -159
  291. package/src/connectors/remote-mcp.ts +0 -1090
  292. package/src/credentials.ts +0 -428
  293. package/src/errors.ts +0 -364
  294. package/src/execute.ts +0 -1406
  295. package/src/executor-admission.ts +0 -328
  296. package/src/executor-result.ts +0 -107
  297. package/src/executors/quickjs-child.ts +0 -172
  298. package/src/executors/quickjs-protocol.ts +0 -99
  299. package/src/executors/quickjs-runtime.ts +0 -470
  300. package/src/executors/quickjs.ts +0 -776
  301. package/src/favicon.ts +0 -53
  302. package/src/index.ts +0 -666
  303. package/src/invocation.ts +0 -655
  304. package/src/json-schema.ts +0 -11
  305. package/src/mcp-result.ts +0 -43
  306. package/src/meta-tools.ts +0 -1607
  307. package/src/node.ts +0 -206
  308. package/src/operator-ui/browser.css +0 -423
  309. package/src/operator-ui/browser.ts +0 -1281
  310. package/src/operator-ui/generated.ts +0 -4
  311. package/src/operator-ui/model.ts +0 -111
  312. package/src/providers/cloudflare.ts +0 -3999
  313. package/src/providers/linear.ts +0 -302
  314. package/src/providers/mixpanel.ts +0 -229
  315. package/src/providers/notion.ts +0 -1879
  316. package/src/providers/stripe.ts +0 -307
  317. package/src/registry.ts +0 -1085
  318. package/src/retired-toolkits.ts +0 -14
  319. package/src/routes/access-tokens.ts +0 -115
  320. package/src/routes/activity.ts +0 -215
  321. package/src/routes/credentials.ts +0 -254
  322. package/src/routes/mcp.ts +0 -429
  323. package/src/routes/oauth.ts +0 -322
  324. package/src/routes/shared.ts +0 -247
  325. package/src/routes/ui.ts +0 -152
  326. package/src/server.ts +0 -195
  327. package/src/skills.ts +0 -271
  328. package/src/storage/file.ts +0 -123
  329. package/src/storage/memory.ts +0 -44
  330. package/src/timeout.ts +0 -84
  331. package/src/tool-safety.ts +0 -15
  332. package/src/types.ts +0 -523
  333. package/src/ui.ts +0 -744
  334. package/src/validate.ts +0 -387
  335. package/src/version.ts +0 -7
package/src/ui.ts DELETED
@@ -1,744 +0,0 @@
1
- import {
2
- credentialTestRule,
3
- describeUndeclaredCredentialFields,
4
- storedCredentialShape,
5
- } from "./credentials.js";
6
- import type { CredentialVault } from "./credentials.js";
7
- import {
8
- closeConnectorScope,
9
- type DeferredWork,
10
- } from "./connector-scope.js";
11
- import {
12
- mapSettledWithConcurrency,
13
- resolveDiscoveryConcurrency,
14
- } from "./concurrency.js";
15
- import {
16
- type CredentialManagementCapability,
17
- type AccessTokenManagementCapability,
18
- type UiConnector,
19
- type UiData,
20
- type UiTool,
21
- } from "./operator-ui/model.js";
22
- import {
23
- OPERATOR_UI_CSS,
24
- OPERATOR_UI_SCRIPT,
25
- } from "./operator-ui/generated.js";
26
- import type { Registry } from "./registry.js";
27
- import type {
28
- ConnectaBranding,
29
- ConnectorStatus,
30
- UiAuthConfig,
31
- } from "./types.js";
32
- import { CONNECTA_VERSION } from "./version.js";
33
-
34
- export {
35
- filterUiConnectors,
36
- type AccessTokenManagementCapability,
37
- type CredentialManagementCapability,
38
- type UiConnector,
39
- type UiData,
40
- } from "./operator-ui/model.js";
41
-
42
- /** Connecta's default monochrome "C" mark. */
43
- export const CONNECTA_FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
44
- <style>
45
- .fg { fill: #000 }
46
- @media (prefers-color-scheme: dark) { .fg { fill: #fff } }
47
- </style>
48
- <path class="fg" d="M27 9.4A13 13 0 1 0 27 22.6l-4.4-2.5a8 8 0 1 1 0-8.2z"/>
49
- </svg>`;
50
-
51
- interface ResolvedBranding {
52
- productName: string;
53
- productUrl?: string;
54
- ownerName?: string;
55
- ownerUrl?: string;
56
- description: string;
57
- /** Browser tab title and page meta name. */
58
- pageTitle: string;
59
- /** href for the page's icon link. */
60
- faviconHref: string;
61
- themeColor: string;
62
- }
63
-
64
- const DEFAULT_FAVICON_HREF = "/favicon.svg";
65
-
66
- /**
67
- * Branding arrives from operator config, which is untyped at a JS call site, so
68
- * every field is treated as `unknown`: a non-string is read as unset rather than
69
- * throwing on `.trim()`. Rendering must degrade to defaults for a malformed
70
- * value, never fail — `createConnecta` calls this during construction.
71
- */
72
- function trimmedString(value: unknown): string | undefined {
73
- return typeof value === "string" ? value.trim() || undefined : undefined;
74
- }
75
-
76
- export function resolveBranding(
77
- branding?: ConnectaBranding,
78
- ): ResolvedBranding {
79
- const productName = trimmedString(branding?.productName) ?? "Connecta";
80
- const ownerName = trimmedString(branding?.ownerName);
81
- // Operator branding URLs become masthead/callback hrefs, so a non-http(s)
82
- // scheme (javascript:, data:) is dropped the same as an unset URL — the
83
- // callers already render a <span> instead of an <a> when it is absent.
84
- const productUrl = trimmedString(branding?.productUrl);
85
- const ownerUrl = trimmedString(branding?.ownerUrl);
86
- const faviconHref = trimmedString(branding?.favicon?.href);
87
- return {
88
- productName,
89
- ...(productUrl && isSafeHttpUrl(productUrl) ? { productUrl } : {}),
90
- ...(ownerName ? { ownerName } : {}),
91
- ...(ownerUrl && isSafeHttpUrl(ownerUrl) ? { ownerUrl } : {}),
92
- description:
93
- trimmedString(branding?.description) ??
94
- `Manage the services this ${productName} instance makes available to agents.`,
95
- pageTitle:
96
- trimmedString(branding?.pageTitle) ??
97
- (ownerName ? `${productName} — ${ownerName}` : productName),
98
- faviconHref:
99
- faviconHref && isSafeIconHref(faviconHref)
100
- ? faviconHref
101
- : DEFAULT_FAVICON_HREF,
102
- themeColor: trimmedString(branding?.themeColor) ?? "#ffffff",
103
- };
104
- }
105
-
106
- /**
107
- * Whether the operator meant to supply a value here — the question every
108
- * dropped-URL warning asks before naming a field, and one definition so the
109
- * branding and `uiAuth` warnings cannot answer it differently. A non-string
110
- * counts as set: the intent was there and is exactly what the warning reports
111
- * on. A blank or whitespace-only string does not; that is indistinguishable
112
- * from leaving the field alone, and both take the default silently.
113
- */
114
- function isSetUrlValue(value: unknown): boolean {
115
- return typeof value === "string"
116
- ? trimmedString(value) !== undefined
117
- : value !== undefined && value !== null;
118
- }
119
-
120
- /**
121
- * A JS string literal safe to inline in a script element. Escaping `/` keeps
122
- * an operator-supplied `</script>` from terminating the element early.
123
- */
124
- function stringForInlineScript(value: string): string {
125
- return JSON.stringify(value).replace(/\//g, "\\/");
126
- }
127
-
128
- /**
129
- * Names of the branding URLs the operator set that failed their gate and were
130
- * replaced by a default. Lives beside the gates so the startup warning cannot
131
- * drift from them, and takes `unknown` fields for the same reason
132
- * `resolveBranding` does — a warning helper must never throw.
133
- */
134
- export function droppedBrandingUrls(branding?: ConnectaBranding): string[] {
135
- if (!branding) return [];
136
- const resolved = resolveBranding(branding);
137
- const faviconHref = branding.favicon?.href;
138
- return [
139
- ...(isSetUrlValue(branding.productUrl) && !resolved.productUrl
140
- ? ["productUrl"]
141
- : []),
142
- ...(isSetUrlValue(branding.ownerUrl) && !resolved.ownerUrl
143
- ? ["ownerUrl"]
144
- : []),
145
- ...(isSetUrlValue(faviconHref) &&
146
- trimmedString(faviconHref) !== resolved.faviconHref
147
- ? ["favicon.href"]
148
- : []),
149
- ];
150
- }
151
-
152
- /**
153
- * True only for absolute `http:`/`https:` URLs. Downstream connectors control
154
- * their `authorizationUrl`, so a hostile/misconfigured one could hand back a
155
- * `javascript:` (or other) scheme; gate it before it can become an href.
156
- */
157
- export function isSafeHttpUrl(url: unknown): boolean {
158
- if (typeof url !== "string") return false;
159
- try {
160
- const scheme = new URL(url).protocol;
161
- return scheme === "http:" || scheme === "https:";
162
- } catch {
163
- return false;
164
- }
165
- }
166
-
167
- /**
168
- * Only the second check's base; any origin works because the check is whether
169
- * the href stays on whatever origin it is resolved against. It is deliberately
170
- * never the sole gate: a value whose own authority equals this host (say
171
- * `//connecta.invalid/x`) would resolve to this exact origin and pass, so the
172
- * structural check below runs first and is what actually rejects `//host`.
173
- */
174
- const SAME_ORIGIN_PROBE = "https://connecta.invalid";
175
-
176
- /** Removed anywhere in a URL by the parser, so a gate must ignore them too. */
177
- const URL_STRIPPED_CHARS = /[\t\n\r]/g;
178
-
179
- /**
180
- * True for values allowed in the page's `<link rel="icon" href>`: an absolute
181
- * `http(s)` URL (an icon the operator hosts elsewhere) or a path rooted at this
182
- * origin. The relative carve-out is deliberate rather than accidental — the
183
- * default href is the relative `/favicon.svg`, which `isSafeHttpUrl` alone would
184
- * reject — and it is kept narrow on both ends.
185
- *
186
- * Root-relative only, because operator and OAuth callback pages sit at
187
- * different depths and a document-relative path would resolve differently.
188
- *
189
- * "Root-relative" is enforced structurally: exactly one leading `/` followed by
190
- * a character that is neither `/` nor `\`. Both of those would make the value an
191
- * authority (`//host`, and `/\host` because the URL parser folds `\` to `/` in
192
- * special schemes), pointing at an origin this server does not control. The test
193
- * runs on a copy with tab/newline/CR removed, since the parser strips those
194
- * anywhere and `/\t/host` would otherwise slip through as single-slash. The
195
- * origin comparison that follows is defense in depth, not the authority check —
196
- * on its own it would accept an authority that happened to equal the probe host.
197
- */
198
- export function isSafeIconHref(href: unknown): boolean {
199
- if (typeof href !== "string") return false;
200
- if (isSafeHttpUrl(href)) return true;
201
- if (!/^\/(?![/\\])/.test(href.replace(URL_STRIPPED_CHARS, ""))) return false;
202
- try {
203
- return new URL(href, SAME_ORIGIN_PROBE).origin === SAME_ORIGIN_PROBE;
204
- } catch {
205
- return false;
206
- }
207
- }
208
-
209
- /**
210
- * True only for an absolute `https:` URL — the gate every `uiAuth` URL passes:
211
- * `frontendApiUrl`, which becomes the operator shell's sign-in loader source,
212
- * and `signInUrl`/`signUpUrl`, which ClerkJS uses as *navigation targets* when
213
- * the operator signs in. With those three gated, no operator-config value
214
- * reaches the browser in a URL position — attribute or navigation — without
215
- * validation, and there is no exception left to remember.
216
- *
217
- * Stricter than `isSafeHttpUrl` on purpose: no `http:` carve-out, no loopback
218
- * carve-out, and no relative form. Nobody types `frontendApiUrl` — the shipped
219
- * Clerk adapter derives it from the publishable key, and Clerk's Frontend API is
220
- * always https — and a cleartext script source on an operator page would be a
221
- * downgrade even where a browser's mixed-content rules had not already blocked
222
- * it. `signInUrl`/`signUpUrl` *are* typed by the operator, but what belongs
223
- * there is a hosted Account Portal address (`https://accounts.<domain>` or
224
- * `https://<slug>.accounts.dev`), which is https as well; `http:` would carry a
225
- * sign-in over cleartext, and a path relative to this origin is meaningless
226
- * because this server hosts no sign-in page of its own. So the looser gate would
227
- * buy nothing real, and the same strictness holds for all three.
228
- */
229
- export function isSafeHttpsUrl(url: unknown): boolean {
230
- if (typeof url !== "string") return false;
231
- try {
232
- return new URL(url).protocol === "https:";
233
- } catch {
234
- return false;
235
- }
236
- }
237
-
238
- /**
239
- * Names of the `uiAuth` URLs an inbound-auth provider supplied that failed their
240
- * gate. Lives beside the gate for the same reason `droppedBrandingUrls` does: the
241
- * startup warning cannot then drift from what rendering actually drops. Every
242
- * field is read defensively rather than trusted, because a custom `InboundAuth`
243
- * is untyped at a JS call site — `isSafeHttpsUrl` takes `unknown`, and a
244
- * `uiAuth` that is not the clerk shape is reported as nothing to warn about.
245
- *
246
- * `frontendApiUrl` is required, so anything that fails its gate is a drop.
247
- * `signInUrl` and `signUpUrl` are optional, so only a value the operator
248
- * *supplied* and the gate then rejected is worth a warning — an unset field
249
- * took no default away from anyone. `isSetUrlValue` decides that, the same way
250
- * and for the same reasons it decides it for the branding URLs: a warning that
251
- * fires for one and not the other would be reporting on the field rather than
252
- * on the operator's intent. Rendering is not consulted for this: it drops on
253
- * the gate alone, and a blank string fails that gate too — it is simply not
254
- * *reported*, because a blank is indistinguishable from leaving the field
255
- * alone.
256
- */
257
- export function droppedUiAuthUrls(uiAuth?: UiAuthConfig): string[] {
258
- if (!uiAuth || uiAuth.kind !== "clerk") return [];
259
- return [
260
- ...(isSafeHttpsUrl(uiAuth.frontendApiUrl) ? [] : ["uiAuth.frontendApiUrl"]),
261
- ...(isSetUrlValue(uiAuth.signInUrl) && !isSafeHttpsUrl(uiAuth.signInUrl)
262
- ? ["uiAuth.signInUrl"]
263
- : []),
264
- ...(isSetUrlValue(uiAuth.signUpUrl) && !isSafeHttpsUrl(uiAuth.signUpUrl)
265
- ? ["uiAuth.signUpUrl"]
266
- : []),
267
- ];
268
- }
269
-
270
- export type OperatorPage =
271
- | "connections"
272
- | "credentials"
273
- | "tokens"
274
- | "activity";
275
-
276
- const OPERATOR_PAGE_LABELS: Readonly<Record<OperatorPage, string>> = {
277
- connections: "Connections",
278
- credentials: "Credentials",
279
- tokens: "Access tokens",
280
- activity: "Activity",
281
- };
282
-
283
- export function operatorPageForPath(path: string): OperatorPage | undefined {
284
- if (path === "/") return "connections";
285
- if (path === "/credentials") return "credentials";
286
- if (path === "/tokens") return "tokens";
287
- if (path === "/activity") return "activity";
288
- return undefined;
289
- }
290
-
291
- export function operatorPageTitle(
292
- page: OperatorPage,
293
- configuredTitle: string,
294
- ): string {
295
- return `${OPERATOR_PAGE_LABELS[page]} — ${configuredTitle}`;
296
- }
297
-
298
- export function credentialManagementCapability(input: {
299
- eligibleClerkOperator: boolean;
300
- hasCredentialSlots: boolean;
301
- hasCredentialVault: boolean;
302
- }): CredentialManagementCapability {
303
- if (!input.eligibleClerkOperator) return "requires_clerk";
304
- if (!input.hasCredentialSlots) return "no_slots";
305
- if (!input.hasCredentialVault) return "vault_not_configured";
306
- return "available";
307
- }
308
-
309
- /**
310
- * Build the read-only status payload served at `/ui/data`. Broken connectors are
311
- * isolated: they surface status "error" with an empty tool list rather than
312
- * failing the whole payload.
313
- */
314
- export async function buildUiData(
315
- registry: Registry,
316
- baseUrl: string,
317
- serverInfo: { name: string; version: string },
318
- credentialVault?: CredentialVault,
319
- activityEnabled = false,
320
- credentialManagement: CredentialManagementCapability = credentialVault
321
- ? "available"
322
- : "requires_clerk",
323
- defer?: DeferredWork,
324
- oauthManagement = false,
325
- discoveryConcurrency?: number,
326
- accessTokenManagement: AccessTokenManagementCapability = "not_configured",
327
- ): Promise<UiData> {
328
- const requestScope = {};
329
- const connectorSet = registry.listConnectors();
330
- const concurrency = resolveDiscoveryConcurrency(discoveryConcurrency);
331
- const settled = await mapSettledWithConcurrency(
332
- connectorSet,
333
- concurrency,
334
- async (c): Promise<UiConnector> => {
335
- const drift = await registry.credentialDriftFor(c.id);
336
- const status: ConnectorStatus = drift
337
- ? { state: "auth_required", message: drift }
338
- : await registry.statusFor(c.id, baseUrl, requestScope);
339
- let tools: UiTool[] = [];
340
- // `status()` on an unauthenticated remote connector starts OAuth and
341
- // stores its state + PKCE verifier. Probing listTools immediately
342
- // afterward would start a second flow, overwrite that state, and return
343
- // the now-stale first URL to the operator. Only inspect tools after
344
- // status proves the connector is healthy.
345
- if (status.state === "ok") {
346
- try {
347
- tools = (
348
- await registry.getTools(c.id, baseUrl, requestScope)
349
- ).map((t) => ({
350
- name: t.name,
351
- address: `${c.id}.${t.name}`,
352
- ...(t.description !== undefined
353
- ? { description: t.description }
354
- : {}),
355
- }));
356
- } catch {
357
- // broken connector: reported via status "error", tools stay empty
358
- }
359
- }
360
- let credential: UiConnector["credential"];
361
- if (c.credential && credentialVault) {
362
- // One rule, shared with the test route: only the hook matching the
363
- // declared credential shape can run, so the button is offered only
364
- // where a click can succeed (src/credentials.ts).
365
- const testRule = credentialTestRule(c);
366
- const credentialFields = (
367
- metadata?: Awaited<ReturnType<CredentialVault["metadata"]>>,
368
- ) =>
369
- c.credential?.fields?.map((field) => {
370
- const fieldMetadata = metadata?.fields?.[field.name];
371
- return {
372
- name: field.name,
373
- label: field.label,
374
- ...(field.description
375
- ? { description: field.description }
376
- : {}),
377
- ...(field.placeholder
378
- ? { placeholder: field.placeholder }
379
- : {}),
380
- inputType: field.inputType ?? "password",
381
- configured: Boolean(fieldMetadata),
382
- ...(fieldMetadata
383
- ? {
384
- lastFour: fieldMetadata.lastFour,
385
- updatedAt: fieldMetadata.updatedAt,
386
- }
387
- : {}),
388
- };
389
- });
390
- try {
391
- const metadata = await credentialVault.metadata(c.id);
392
- const fields = credentialFields(metadata);
393
- const shape = storedCredentialShape(
394
- c.credential,
395
- metadata?.fields ?? null,
396
- );
397
- credential = {
398
- label: c.credential.label,
399
- ...(c.credential.description
400
- ? { description: c.credential.description }
401
- : {}),
402
- ...(c.credential.placeholder
403
- ? { placeholder: c.credential.placeholder }
404
- : {}),
405
- ...(fields?.length ? { fields } : {}),
406
- configured: shape.state === "valid",
407
- removable: Boolean(metadata),
408
- ...(metadata
409
- ? {
410
- lastFour: metadata.lastFour,
411
- updatedAt: metadata.updatedAt,
412
- }
413
- : {}),
414
- testable:
415
- testRule.mode !== null && shape.state !== "mismatch",
416
- ...(shape.state === "mismatch"
417
- ? { error: shape.message }
418
- : {}),
419
- // A dropped field leaves its secret in the vault, and the field
420
- // list below only renders fields the connector still declares —
421
- // so without this line there is nowhere an operator could see it.
422
- ...(shape.state === "valid" && shape.undeclared.length
423
- ? {
424
- notice: describeUndeclaredCredentialFields(
425
- shape.undeclared,
426
- ),
427
- }
428
- : {}),
429
- };
430
- } catch {
431
- const fields = credentialFields();
432
- credential = {
433
- label: c.credential.label,
434
- ...(c.credential.description
435
- ? { description: c.credential.description }
436
- : {}),
437
- ...(c.credential.placeholder
438
- ? { placeholder: c.credential.placeholder }
439
- : {}),
440
- ...(fields?.length ? { fields } : {}),
441
- configured: false,
442
- removable: true,
443
- testable: testRule.mode !== null,
444
- error: "Stored credential could not be read.",
445
- };
446
- }
447
- }
448
- return {
449
- id: c.id,
450
- ...(c.title ? { title: c.title } : {}),
451
- ...(c.description !== undefined
452
- ? { description: c.description }
453
- : {}),
454
- status: status.state,
455
- ...(status.message ? { message: status.message } : {}),
456
- ...(isSafeHttpUrl(status.authorizationUrl)
457
- ? { authorizationUrl: status.authorizationUrl }
458
- : {}),
459
- toolCount: tools.length,
460
- tools,
461
- ...(c.disconnectAuth && c.startAuth ? { oauth: true } : {}),
462
- ...(credential ? { credential } : {}),
463
- };
464
- },
465
- );
466
- await mapSettledWithConcurrency(
467
- connectorSet,
468
- concurrency,
469
- (connector) =>
470
- closeConnectorScope(
471
- connector,
472
- registry.contextFor(connector.id, baseUrl, requestScope),
473
- defer,
474
- ),
475
- );
476
- const connectors = settled.map((result) => {
477
- if (result.status === "rejected") throw result.reason;
478
- return result.value;
479
- });
480
- return {
481
- serverInfo,
482
- connectaVersion: CONNECTA_VERSION,
483
- connectors,
484
- activityEnabled,
485
- credentialManagement,
486
- accessTokenManagement,
487
- oauthManagement,
488
- };
489
- }
490
-
491
- function escapeHtmlAttr(value: string): string {
492
- return value
493
- .replaceAll("&", "&amp;")
494
- .replaceAll('"', "&quot;")
495
- .replaceAll("<", "&lt;")
496
- .replaceAll(">", "&gt;");
497
- }
498
-
499
- function jsonForInlineScript(value: unknown): string {
500
- return JSON.stringify(value)
501
- .replaceAll("<", "\\u003c")
502
- .replaceAll(">", "\\u003e")
503
- .replaceAll("&", "\\u0026");
504
- }
505
-
506
- /**
507
- * Every operator page serves this same data-free shell. Connector, credential,
508
- * and activity data arrives only through the authenticated `/ui/*` APIs.
509
- */
510
- export function renderUiHtml(
511
- uiAuth?: UiAuthConfig,
512
- mcpUrl = "/mcp",
513
- branding?: ConnectaBranding,
514
- nonce?: string,
515
- page: OperatorPage = "connections",
516
- ): string {
517
- const clerk = uiAuth?.kind === "clerk" ? uiAuth : undefined;
518
- // The Clerk loader's origin. A value that fails the gate is dropped rather
519
- // than escaped into the page: the loader tag is simply not emitted, the gate
520
- // reports that Clerk could not load, and the rest of the shell still renders —
521
- // the same fallback-and-warn posture the branding URLs take, with the drop
522
- // named in a startup warning (see `droppedUiAuthUrls`).
523
- const clerkScriptOrigin =
524
- clerk && isSafeHttpsUrl(clerk.frontendApiUrl)
525
- ? clerk.frontendApiUrl
526
- : undefined;
527
- // Enumerated field by field, because this object is serialized into the page's
528
- // inline script: a rejected frontendApiUrl must not reach the document through
529
- // `AUTH` after being kept out of the `<script src>`, and a rejected
530
- // signInUrl/signUpUrl — which `AUTH` is the only path into the page for — must
531
- // not reach it at all. Dropping one leaves the key absent, so `Clerk.load`
532
- // falls back to its own default the same way it does for an unset value.
533
- const auth = clerk
534
- ? {
535
- kind: clerk.kind,
536
- publishableKey: clerk.publishableKey,
537
- ...(clerkScriptOrigin ? { frontendApiUrl: clerkScriptOrigin } : {}),
538
- ...(isSafeHttpsUrl(clerk.signInUrl)
539
- ? { signInUrl: clerk.signInUrl }
540
- : {}),
541
- ...(isSafeHttpsUrl(clerk.signUpUrl)
542
- ? { signUpUrl: clerk.signUpUrl }
543
- : {}),
544
- }
545
- : (uiAuth ?? { kind: "bearer" as const });
546
- const brand = resolveBranding(branding);
547
- const title = operatorPageTitle(page, brand.pageTitle);
548
- // When an operator shell ships a nonce-based CSP, every script it emits must
549
- // carry that nonce to run; without a nonce the markup is unchanged.
550
- const nonceAttr = nonce ? ` nonce="${nonce}"` : "";
551
- // Top-left corner. With an owner set it reads "<owner> <product>"; without
552
- // one the product label stands alone. Either half links out when the
553
- // matching URL is configured.
554
- const owner = brand.ownerName
555
- ? brand.ownerUrl
556
- ? `<a class="brand navlink" href="${escapeHtmlAttr(brand.ownerUrl)}">${escapeHtmlAttr(brand.ownerName)}</a>`
557
- : `<span class="brand">${escapeHtmlAttr(brand.ownerName)}</span>`
558
- : brand.productUrl
559
- ? `<a class="brand navlink" href="${escapeHtmlAttr(brand.productUrl)}">${escapeHtmlAttr(brand.productName)}</a>`
560
- : `<span class="brand">${escapeHtmlAttr(brand.productName)}</span>`;
561
- const product = brand.ownerName
562
- ? brand.productUrl
563
- ? `<a class="product navlink" href="${escapeHtmlAttr(brand.productUrl)}">${escapeHtmlAttr(brand.productName)}</a>`
564
- : `<span class="product">${escapeHtmlAttr(brand.productName)}</span>`
565
- : "";
566
- const clerkScript =
567
- clerk && clerkScriptOrigin
568
- ? `<script${nonceAttr} defer crossorigin="anonymous" data-clerk-publishable-key="${escapeHtmlAttr(clerk.publishableKey)}" src="${escapeHtmlAttr(clerkScriptOrigin)}/npm/@clerk/clerk-js@6/dist/clerk.browser.js"></script>`
569
- : "";
570
-
571
- return `<!doctype html>
572
- <html lang="en">
573
- <head>
574
- <meta charset="utf-8">
575
- <meta name="viewport" content="width=device-width, initial-scale=1">
576
- <meta name="theme-color" content="${escapeHtmlAttr(brand.themeColor)}">
577
- <meta name="description" content="${escapeHtmlAttr(brand.description)}">
578
- <link rel="icon" href="${escapeHtmlAttr(brand.faviconHref)}" type="image/svg+xml">
579
- <link rel="shortcut icon" href="/favicon.ico">
580
- <title>${escapeHtmlAttr(title)}</title>
581
- ${clerkScript}
582
- <style>${OPERATOR_UI_CSS}</style>
583
- </head>
584
- <body>
585
- <a class="skip-link" href="#operatorContent">Skip to operator page</a>
586
- <header class="masthead shell pgrid">
587
- ${owner}
588
- <div class="mast-nav">
589
- ${product}
590
- <div id="appNav" class="mast-actions hidden">
591
- <nav class="page-nav" aria-label="Operator pages">
592
- <a id="connectionsNav" class="navlink" href="/"
593
- data-operator-page="connections"${page === "connections" ? ' aria-current="page"' : ""}>Connections</a>
594
- <a id="credentialsNav" class="navlink hidden" href="/credentials"
595
- data-operator-page="credentials"${page === "credentials" ? ' aria-current="page"' : ""}>Credentials</a>
596
- <a id="tokensNav" class="navlink hidden" href="/tokens"
597
- data-operator-page="tokens"${page === "tokens" ? ' aria-current="page"' : ""}>Access tokens</a>
598
- <a id="activityNav" class="navlink hidden" href="/activity"
599
- data-operator-page="activity"${page === "activity" ? ' aria-current="page"' : ""}>Activity</a>
600
- </nav>
601
- <div class="session-actions" aria-label="Session actions">
602
- <button id="change" class="navlink hidden" type="button">Change token</button>
603
- <button id="signout" class="navlink hidden" type="button">Sign out</button>
604
- </div>
605
- </div>
606
- </div>
607
- </header>
608
-
609
- <main id="operatorContent" class="page shell" tabindex="-1">
610
- <section id="gate" class="hidden">
611
- <div class="lead pgrid">
612
- <h1 id="gateHeading" class="pcap" tabindex="-1">${OPERATOR_PAGE_LABELS[page]}</h1>
613
- <div class="pbody lead-copy">
614
- <p>${escapeHtmlAttr(brand.description)}</p>
615
- <p id="gateCopy" class="meta"></p>
616
- <div id="tokenGate" class="row gate-actions hidden">
617
- <input id="token" type="password" placeholder="Bearer token" autocomplete="off"
618
- aria-label="Bearer token">
619
- <button id="save" class="linklike" type="button">Open operator pages</button>
620
- </div>
621
- <div id="clerkGate" class="actions gate-actions hidden">
622
- <button id="signin" class="linklike" type="button">Team sign in</button>
623
- <button id="gateSignout" class="linklike hidden" type="button">Sign out</button>
624
- </div>
625
- <p id="err" role="alert"></p>
626
- </div>
627
- </div>
628
- </section>
629
-
630
- <div id="app" class="hidden">
631
- <section id="connectionsView"${page === "connections" ? "" : ' class="hidden"'}>
632
- <div class="lead pgrid">
633
- <h1 id="connectionsHeading" class="pcap" tabindex="-1">Connections</h1>
634
- <div class="pbody lead-copy">
635
- <p>Use this endpoint to give an MCP client access to the tools below.</p>
636
- <div class="endpoint">
637
- <div class="endpoint-row">
638
- <code id="mcpUrl" class="mono"></code>
639
- <button id="copyMcpUrl" class="linklike" type="button">Copy URL</button>
640
- </div>
641
- </div>
642
- <p class="cap" id="serverInfo">${escapeHtmlAttr(brand.productName)} operator</p>
643
- <p id="oauthNotice" class="meta" role="status" aria-live="polite" tabindex="-1"></p>
644
- </div>
645
- </div>
646
- <section class="section pgrid" aria-labelledby="connectorLedgerHeading">
647
- <h2 class="pcap" id="connectorLedgerHeading">Connectors</h2>
648
- <div class="pbody">
649
- <div class="row toolbar">
650
- <input id="filter" type="search" placeholder="Filter connectors or tools…"
651
- aria-label="Filter connectors or tools">
652
- </div>
653
- <div id="list" class="connector-tools" aria-busy="false"></div>
654
- </div>
655
- </section>
656
- </section>
657
-
658
- <section id="credentialsView"${page === "credentials" ? "" : ' class="hidden"'}>
659
- <div class="lead pgrid">
660
- <h1 id="credentialsHeading" class="pcap" tabindex="-1">Credentials</h1>
661
- <div class="pbody">
662
- <p class="activity-copy">Rotate operator-managed connector credentials. Stored values are never returned or displayed.</p>
663
- <p id="credentialNotice" class="meta" role="status" aria-live="polite"
664
- tabindex="-1"></p>
665
- <div id="credentialUnavailable" class="unavailable hidden"></div>
666
- <div id="credentialList" class="credential-ledger" aria-busy="false"></div>
667
- </div>
668
- </div>
669
- </section>
670
-
671
- <section id="tokensView"${page === "tokens" ? "" : ' class="hidden"'}>
672
- <div class="lead pgrid">
673
- <h1 id="tokensHeading" class="pcap" tabindex="-1">Access tokens</h1>
674
- <div class="pbody">
675
- <p class="activity-copy">Create named Bearer tokens for MCP clients. Each secret is shown once; revoke it when that client should lose access.</p>
676
- <p id="tokenNotice" class="meta" role="status" aria-live="polite"
677
- tabindex="-1"></p>
678
- <div id="tokenUnavailable" class="unavailable hidden"></div>
679
- <div id="tokenAvailable" class="hidden">
680
- <form id="tokenCreateForm" class="token-create">
681
- <label for="tokenName">Client name</label>
682
- <div class="row">
683
- <input id="tokenName" type="text" maxlength="80"
684
- placeholder="Claude desktop, ChatGPT production…"
685
- autocomplete="off">
686
- <button id="createToken" class="linklike" type="submit">Create token</button>
687
- </div>
688
- </form>
689
- <section id="tokenReveal" class="token-reveal hidden"
690
- aria-labelledby="tokenRevealHeading">
691
- <div class="token-reveal-head">
692
- <h2 id="tokenRevealHeading" tabindex="-1">Copy this token now</h2>
693
- <span class="cap">Shown once</span>
694
- </div>
695
- <p class="meta">Store it in the MCP client before leaving this page. It cannot be displayed again.</p>
696
- <div class="endpoint-row token-secret">
697
- <code id="createdToken" class="mono"></code>
698
- <button id="copyCreatedToken" class="linklike" type="button">Copy token</button>
699
- </div>
700
- <button id="dismissCreatedToken" class="linklike" type="button">I stored it</button>
701
- </section>
702
- <div id="tokenList" class="token-ledger" aria-busy="false"></div>
703
- </div>
704
- </div>
705
- </div>
706
- </section>
707
-
708
- <section id="activityView"${page === "activity" ? "" : ' class="hidden"'}>
709
- <div class="lead pgrid">
710
- <h1 id="activityHeading" class="pcap" tabindex="-1">Activity</h1>
711
- <div class="pbody">
712
- <p class="activity-copy" id="activitySummary">Arguments and results are never stored.</p>
713
- <div id="activityUnavailable" class="unavailable hidden">
714
- Activity history is not configured. Add an <span class="mono">activity.store</span>
715
- with a list reader to enable this page.
716
- </div>
717
- <div id="activityAvailable">
718
- <div class="row activity-controls">
719
- <input id="activitySearch" type="search"
720
- placeholder="Search user, tool, or outcome…"
721
- aria-label="Search loaded activity">
722
- <button id="refreshActivity" class="linklike" type="button">Refresh</button>
723
- </div>
724
- <p id="activityNotice" class="meta" role="status" aria-live="polite"></p>
725
- <div id="activityList" class="activity-ledger" aria-busy="false"></div>
726
- <button id="moreActivity" class="linklike activity-more hidden" type="button">Load older</button>
727
- </div>
728
- </div>
729
- </div>
730
- </section>
731
- </div>
732
- </main>
733
-
734
- <script${nonceAttr}>
735
- const AUTH = ${jsonForInlineScript(auth)};
736
- const MCP_URL = ${jsonForInlineScript(mcpUrl)};
737
- const INITIAL_PAGE = ${jsonForInlineScript(page)};
738
- const TITLE_SUFFIX = ${jsonForInlineScript(brand.pageTitle)};
739
- const PRODUCT_NAME = ${stringForInlineScript(brand.productName)};
740
- const PRODUCT_OPERATOR_LABEL = ${stringForInlineScript(brand.productName + " operator")};
741
- ${OPERATOR_UI_SCRIPT}</script>
742
- </body>
743
- </html>`;
744
- }