@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,240 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ActivityActor, ActivityReadGate, ActivityStore } from "../activity.js";
3
+ import type { CredentialVault } from "../credentials.js";
4
+ import type { DeferredWork } from "../connector-scope.js";
5
+ import type { AdmissionController } from "../executor-admission.js";
6
+ import type { Registry } from "../registry.js";
7
+ import type {
8
+ ConnectaBranding,
9
+ Executor,
10
+ InboundAuth,
11
+ Logger,
12
+ } from "../types.js";
13
+ import { operatorPageForPath } from "../ui.js";
14
+
15
+ export interface ServerOptions {
16
+ registry: Registry;
17
+ auth: InboundAuth[];
18
+ publicUrl?: string;
19
+ // The SDK's Implementation shape: name/version plus optional title,
20
+ // websiteUrl, and icons (MCP icons spec) that clients may render.
21
+ serverInfo: ConstructorParameters<typeof McpServer>[0];
22
+ logger: Logger;
23
+ activity?: ActivityStore;
24
+ activityReadGate?: ActivityReadGate;
25
+ activityDeploymentId?: string;
26
+ deploymentInfo?: Record<string, unknown>;
27
+ /** Deadline for call_tool/batch_call calls that pass no timeoutMs. Off when unset. */
28
+ defaultToolTimeoutMs?: number;
29
+ /** Per-connector deadline for the list/search/describe probe fan-out. Default 30_000. */
30
+ probeTimeoutMs?: number;
31
+ /** Maximum simultaneous connector discovery operations. Default 4. */
32
+ discoveryConcurrency?: number;
33
+ /** When set, the execute_code meta-tool is registered on top of the nine. */
34
+ executor?: Executor;
35
+ /** Global FIFO boundary for all non-preflight `/mcp` requests. */
36
+ requestAdmission: AdmissionController;
37
+ /** Encrypted connector-credential storage backing the Credentials page. */
38
+ credentialVault?: CredentialVault;
39
+ /** Optional browser UI and OAuth result-page labels. */
40
+ branding?: ConnectaBranding;
41
+ }
42
+
43
+ export interface RuntimeExecutionContext {
44
+ waitUntil(promise: Promise<unknown>): void;
45
+ }
46
+
47
+ export interface RouteContext {
48
+ request: Request;
49
+ url: URL;
50
+ path: string;
51
+ baseUrl: string;
52
+ opts: ServerOptions;
53
+ defer: DeferredWork | undefined;
54
+ runtimeContext: RuntimeExecutionContext | undefined;
55
+ }
56
+
57
+ export function msg(err: unknown): string {
58
+ return err instanceof Error ? err.message : String(err);
59
+ }
60
+
61
+ export function privateJson(
62
+ body: unknown,
63
+ init: ResponseInit = {},
64
+ ): Response {
65
+ const headers = new Headers(init.headers);
66
+ headers.set("Content-Type", "application/json");
67
+ headers.set("Cache-Control", "no-store");
68
+ headers.set("Referrer-Policy", "no-referrer");
69
+ return new Response(JSON.stringify(body), { ...init, headers });
70
+ }
71
+
72
+ const MAX_LOGGED_VALUE_LENGTH = 64;
73
+
74
+ /**
75
+ * Bounded, escaped form of a caller-influenced value (an identity id or OAuth
76
+ * callback connector id) for the operator log. Goes
77
+ * through JSON.stringify so a caller-controlled newline or control character
78
+ * cannot forge a log line, plus a hand-rolled escape for U+2028/U+2029, which
79
+ * JSON.stringify leaves raw even though a log reader treats them as line
80
+ * terminators. Truncated to a small shared cap so an oversized value cannot
81
+ * flood the log either.
82
+ */
83
+ export function loggableValue(requested: string): string {
84
+ const bounded = requested.slice(0, MAX_LOGGED_VALUE_LENGTH);
85
+ const escaped = JSON.stringify(bounded).replace(
86
+ /[\u2028\u2029]/g,
87
+ (ch) => `\\u${ch.charCodeAt(0).toString(16)}`,
88
+ );
89
+ return escaped + (bounded.length < requested.length ? " (truncated)" : "");
90
+ }
91
+
92
+ const ACTIVITY_ACTOR_NAMESPACE_RE = /^[\x21-\x7e]{1,256}$/;
93
+
94
+ export function activityActorNamespace(
95
+ provider: InboundAuth,
96
+ ): string | undefined {
97
+ return typeof provider.activityActorNamespace === "string" &&
98
+ ACTIVITY_ACTOR_NAMESPACE_RE.test(provider.activityActorNamespace)
99
+ ? provider.activityActorNamespace
100
+ : undefined;
101
+ }
102
+
103
+ export async function authorize(
104
+ request: Request,
105
+ baseUrl: string,
106
+ auth: InboundAuth[],
107
+ ): Promise<
108
+ | {
109
+ ok: true;
110
+ actor: ActivityActor;
111
+ /** True only when the admitting provider can also authorize UI mutation. */
112
+ uiAdminEligible?: boolean;
113
+ }
114
+ | { ok: false; response: Response }
115
+ > {
116
+ if (auth.length === 0) {
117
+ return { ok: true, actor: { kind: "anonymous" } };
118
+ }
119
+ let lastResponse: Response | null = null;
120
+ for (const provider of auth) {
121
+ const result = await provider.authorize(request, baseUrl);
122
+ if (result.ok) {
123
+ const subjectId = result.subjectId ?? result.userId;
124
+ const actorNamespace = activityActorNamespace(provider);
125
+ return {
126
+ ok: true,
127
+ actor: {
128
+ kind: provider.kind,
129
+ ...(subjectId ? { id: subjectId } : {}),
130
+ ...(subjectId && actorNamespace
131
+ ? { namespace: actorNamespace }
132
+ : {}),
133
+ },
134
+ ...(result.userId && provider.uiAuth?.kind === "clerk"
135
+ ? { uiAdminEligible: true }
136
+ : {}),
137
+ };
138
+ }
139
+ lastResponse = result.response;
140
+ }
141
+ return {
142
+ ok: false,
143
+ response:
144
+ lastResponse ??
145
+ new Response(JSON.stringify({ error: "unauthorized" }), {
146
+ status: 401,
147
+ headers: {
148
+ "Content-Type": "application/json",
149
+ "WWW-Authenticate": "Bearer",
150
+ },
151
+ }),
152
+ };
153
+ }
154
+
155
+ export async function authorizeUiAdmin(
156
+ request: Request,
157
+ baseUrl: string,
158
+ auth: InboundAuth[],
159
+ purpose = "credential management",
160
+ ): Promise<{ ok: true; userId: string } | { ok: false; response: Response }> {
161
+ // Operator mutation is intentionally narrower than /mcp and /ui/data: only
162
+ // an interactive Clerk provider may admit it. A static bearer token is useful
163
+ // for headless tool calls but must not become a deployment-admin key.
164
+ //
165
+ // Every Clerk provider gets a turn, the way the /mcp gate does. Stopping at
166
+ // the first would make admission depend on config order: a failed gate or
167
+ // missing user may simply mean a later provider is the one meant to admit.
168
+ // The last refusal is returned if none do.
169
+ const providers = auth.filter(
170
+ (candidate) => candidate.uiAuth?.kind === "clerk",
171
+ );
172
+ if (providers.length === 0) {
173
+ return {
174
+ ok: false,
175
+ response: privateJson(
176
+ { error: `${purpose} requires Clerk authentication` },
177
+ { status: 403 },
178
+ ),
179
+ };
180
+ }
181
+ let lastResponse: Response | null = null;
182
+ for (const provider of providers) {
183
+ const result = await provider.authorize(request, baseUrl);
184
+ if (!result.ok) {
185
+ lastResponse = result.response;
186
+ continue;
187
+ }
188
+ if (!result.userId) {
189
+ lastResponse = privateJson(
190
+ { error: "authenticated user required" },
191
+ { status: 403 },
192
+ );
193
+ continue;
194
+ }
195
+ return { ok: true, userId: result.userId };
196
+ }
197
+ return {
198
+ ok: false,
199
+ response:
200
+ lastResponse ??
201
+ privateJson({ error: "forbidden" }, { status: 403 }),
202
+ };
203
+ }
204
+
205
+ export function isSameOrigin(request: Request, baseUrl: string): boolean {
206
+ const origin = request.headers.get("origin");
207
+ if (!origin) return false;
208
+ try {
209
+ return new URL(origin).origin === new URL(baseUrl).origin;
210
+ } catch {
211
+ return false;
212
+ }
213
+ }
214
+
215
+ export function withSecurityHeaders(
216
+ response: Response,
217
+ requestUrl: URL,
218
+ path: string,
219
+ ): Response {
220
+ const headers = new Headers(response.headers);
221
+ headers.set("X-Content-Type-Options", "nosniff");
222
+ headers.set("Referrer-Policy", "no-referrer");
223
+ if (requestUrl.protocol === "https:") {
224
+ headers.set("Strict-Transport-Security", "max-age=31536000");
225
+ }
226
+ if (operatorPageForPath(path) || path === "/ui") {
227
+ // Operator HTML responses ship their own nonce-based script CSP (which
228
+ // already includes frame-ancestors 'none'); only fall back to the
229
+ // framing-only directive when no CSP is present (for example redirects).
230
+ if (!headers.has("Content-Security-Policy")) {
231
+ headers.set("Content-Security-Policy", "frame-ancestors 'none'");
232
+ }
233
+ headers.set("X-Frame-Options", "DENY");
234
+ }
235
+ return new Response(response.body, {
236
+ status: response.status,
237
+ statusText: response.statusText,
238
+ headers,
239
+ });
240
+ }
@@ -0,0 +1,143 @@
1
+ import { CONNECTA_FAVICON_ICO } from "../favicon.js";
2
+ import {
3
+ buildUiData,
4
+ CONNECTA_FAVICON_SVG,
5
+ credentialManagementCapability,
6
+ operatorPageForPath,
7
+ renderUiHtml,
8
+ } from "../ui.js";
9
+ import {
10
+ authorize,
11
+ privateJson,
12
+ type RouteContext,
13
+ } from "./shared.js";
14
+
15
+ /**
16
+ * Headers that make an operator-supplied favicon body inert on this origin.
17
+ * The SVG route is the sharp one: `image/svg+xml` is an *active* content type,
18
+ * so a `<script>` inside a branding SVG would run on the deployment origin the
19
+ * moment anyone navigated straight to `/favicon.svg` — strictly more powerful
20
+ * than the `favicon.href` vector the branding gates close, because the payload
21
+ * is same-origin. Neutralizing the response rather than inspecting the body
22
+ * keeps every valid static SVG (the built-in mark included) byte-identical:
23
+ *
24
+ * - `sandbox` (no tokens ⇒ every restriction) drops the document into an opaque
25
+ * origin with scripting off, so even a script that ran would have nothing to
26
+ * reach.
27
+ * - `default-src 'none'` denies script, network, and framing outright.
28
+ * - `style-src 'unsafe-inline'` is the single allowance: the default mark styles
29
+ * itself inline to follow the OS colour scheme, and CSS cannot script.
30
+ * - `nosniff` keeps the declared type authoritative in both directions — an SVG
31
+ * can never be re-read as HTML, and `.ico` bytes can never be re-read as SVG.
32
+ *
33
+ * `.ico` bodies are deliberately in scope: they are inert bytes rather than
34
+ * active content, so they are still served verbatim, but they carry the same
35
+ * headers so the invariant is "every favicon route is neutralized" rather than
36
+ * "whichever route got attention".
37
+ */
38
+ const INERT_ICON_HEADERS = {
39
+ "Content-Security-Policy":
40
+ "default-src 'none'; style-src 'unsafe-inline'; sandbox",
41
+ "X-Content-Type-Options": "nosniff",
42
+ };
43
+
44
+ /** Per-request base64 nonce for an operator shell's scripts (Node 20+ and Workers). */
45
+ function uiScriptNonce(): string {
46
+ const bytes = crypto.getRandomValues(new Uint8Array(16));
47
+ let binary = "";
48
+ for (const byte of bytes) binary += String.fromCharCode(byte);
49
+ return btoa(binary);
50
+ }
51
+
52
+ export async function routeUi(
53
+ context: RouteContext,
54
+ ): Promise<Response | null> {
55
+ const { request, url, path, baseUrl, opts, defer } = context;
56
+ if (request.method === "GET" && path === "/favicon.svg") {
57
+ return new Response(opts.branding?.favicon?.svg ?? CONNECTA_FAVICON_SVG, {
58
+ headers: {
59
+ "Content-Type": "image/svg+xml",
60
+ "Cache-Control": "public, max-age=86400",
61
+ ...INERT_ICON_HEADERS,
62
+ },
63
+ });
64
+ }
65
+ if (request.method === "GET" && path === "/favicon.ico") {
66
+ return new Response(opts.branding?.favicon?.ico ?? CONNECTA_FAVICON_ICO, {
67
+ headers: {
68
+ "Content-Type": "image/x-icon",
69
+ "Cache-Control": "public, max-age=86400",
70
+ ...INERT_ICON_HEADERS,
71
+ },
72
+ });
73
+ }
74
+ if (path === "/ui") {
75
+ if (request.method !== "GET") {
76
+ return privateJson({ error: "method not allowed" }, { status: 405 });
77
+ }
78
+ const target = new URL(`/${url.search}`, baseUrl);
79
+ return new Response(null, {
80
+ status: 308,
81
+ headers: { Location: target.toString() },
82
+ });
83
+ }
84
+
85
+ const operatorPage = operatorPageForPath(path);
86
+ if (operatorPage) {
87
+ if (request.method !== "GET" && request.method !== "HEAD") {
88
+ return privateJson({ error: "method not allowed" }, { status: 405 });
89
+ }
90
+ // Open shell — carries no operator data; everything comes from the
91
+ // authenticated /ui/* APIs after the browser establishes a session.
92
+ const uiAuth = opts.auth.find((provider) => provider.uiAuth)?.uiAuth;
93
+ const mcpUrl = new URL("/mcp", baseUrl).toString();
94
+ // Nonce the page's inline script (and the Clerk loader). 'strict-dynamic'
95
+ // lets scripts the nonced Clerk loader injects at runtime execute; the
96
+ // https:/'unsafe-inline' fallbacks are ignored by CSP3 browsers that
97
+ // honour the nonce and only cover legacy ones. No default-src, so Clerk's
98
+ // style/font/network needs and the page's inline <style> stay unrestricted
99
+ // — only script execution, the XSS sink, is gated.
100
+ const nonce = uiScriptNonce();
101
+ return new Response(
102
+ request.method === "HEAD"
103
+ ? null
104
+ : renderUiHtml(uiAuth, mcpUrl, opts.branding, nonce, operatorPage),
105
+ {
106
+ status: 200,
107
+ headers: {
108
+ "Content-Type": "text/html; charset=utf-8",
109
+ "Content-Security-Policy":
110
+ `script-src 'nonce-${nonce}' 'strict-dynamic' https: 'unsafe-inline'; ` +
111
+ "object-src 'none'; base-uri 'none'; frame-ancestors 'none'",
112
+ "X-Content-Type-Options": "nosniff",
113
+ },
114
+ },
115
+ );
116
+ }
117
+ if (path !== "/ui/data") return null;
118
+
119
+ const authz = await authorize(request, baseUrl, opts.auth);
120
+ if (!authz.ok) return authz.response;
121
+ const eligibleClerkOperator = authz.uiAdminEligible === true;
122
+ const credentialManagement = credentialManagementCapability({
123
+ eligibleClerkOperator,
124
+ hasCredentialSlots: opts.registry
125
+ .listConnectors()
126
+ .some((connector) => Boolean(connector.credential)),
127
+ hasCredentialVault: Boolean(opts.credentialVault),
128
+ });
129
+ const data = await buildUiData(
130
+ opts.registry,
131
+ baseUrl,
132
+ opts.serverInfo,
133
+ // The static headless bearer may read connector health, but only a
134
+ // Clerk-authenticated operator receives credential metadata.
135
+ eligibleClerkOperator ? opts.credentialVault : undefined,
136
+ Boolean(opts.activity?.list),
137
+ credentialManagement,
138
+ defer,
139
+ eligibleClerkOperator,
140
+ opts.discoveryConcurrency,
141
+ );
142
+ return privateJson(data);
143
+ }