@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.
- package/CHANGELOG.md +135 -0
- package/README.md +47 -148
- package/dist/activity.d.ts +0 -8
- package/dist/activity.d.ts.map +1 -1
- package/dist/activity.js +0 -1
- package/dist/activity.js.map +1 -1
- package/dist/auth/bearer.d.ts +1 -7
- package/dist/auth/bearer.d.ts.map +1 -1
- package/dist/auth/bearer.js +3 -24
- package/dist/auth/bearer.js.map +1 -1
- package/dist/auth/clerk.d.ts +2 -11
- package/dist/auth/clerk.d.ts.map +1 -1
- package/dist/auth/clerk.js +31 -46
- package/dist/auth/clerk.js.map +1 -1
- package/dist/call-admission.d.ts.map +1 -1
- package/dist/call-admission.js +9 -4
- package/dist/call-admission.js.map +1 -1
- package/dist/catalog-fingerprint.d.ts +20 -0
- package/dist/catalog-fingerprint.d.ts.map +1 -0
- package/dist/catalog-fingerprint.js +34 -0
- package/dist/catalog-fingerprint.js.map +1 -0
- package/dist/catalog-limits.d.ts +10 -0
- package/dist/catalog-limits.d.ts.map +1 -0
- package/dist/catalog-limits.js +10 -0
- package/dist/catalog-limits.js.map +1 -0
- package/dist/catalog-service.d.ts +137 -0
- package/dist/catalog-service.d.ts.map +1 -0
- package/dist/catalog-service.js +401 -0
- package/dist/catalog-service.js.map +1 -0
- package/dist/catalog.d.ts +6 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +53 -2
- package/dist/catalog.js.map +1 -1
- package/dist/concurrency.d.ts +4 -0
- package/dist/concurrency.d.ts.map +1 -0
- package/dist/concurrency.js +31 -0
- package/dist/concurrency.js.map +1 -0
- package/dist/connectors/api.d.ts.map +1 -1
- package/dist/connectors/api.js +22 -12
- package/dist/connectors/api.js.map +1 -1
- package/dist/connectors/remote-mcp.d.ts.map +1 -1
- package/dist/connectors/remote-mcp.js +53 -41
- package/dist/connectors/remote-mcp.js.map +1 -1
- package/dist/credentials.d.ts +3 -2
- package/dist/credentials.d.ts.map +1 -1
- package/dist/errors.d.ts +19 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/execute.d.ts +10 -16
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +143 -330
- package/dist/execute.js.map +1 -1
- package/dist/executor-result.d.ts +0 -3
- package/dist/executor-result.d.ts.map +1 -1
- package/dist/executor-result.js +3 -3
- package/dist/executor-result.js.map +1 -1
- package/dist/executors/quickjs-child.js +3 -3
- package/dist/executors/quickjs-child.js.map +1 -1
- package/dist/executors/quickjs-protocol.d.ts +4 -1
- package/dist/executors/quickjs-protocol.d.ts.map +1 -1
- package/dist/executors/quickjs-runtime.d.ts.map +1 -1
- package/dist/executors/quickjs-runtime.js +7 -2
- package/dist/executors/quickjs-runtime.js.map +1 -1
- package/dist/executors/quickjs.d.ts.map +1 -1
- package/dist/executors/quickjs.js +4 -1
- package/dist/executors/quickjs.js.map +1 -1
- package/dist/favicon.js +2 -2
- package/dist/favicon.js.map +1 -1
- package/dist/index.d.ts +8 -82
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +58 -104
- package/dist/index.js.map +1 -1
- package/dist/invocation.d.ts +101 -0
- package/dist/invocation.d.ts.map +1 -0
- package/dist/invocation.js +405 -0
- package/dist/invocation.js.map +1 -0
- package/dist/meta-tools.d.ts +9 -95
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +235 -725
- package/dist/meta-tools.js.map +1 -1
- package/dist/operator-ui/generated.d.ts +3 -0
- package/dist/operator-ui/generated.d.ts.map +1 -0
- package/dist/operator-ui/generated.js +5 -0
- package/dist/operator-ui/generated.js.map +1 -0
- package/dist/operator-ui/model.d.ts +73 -0
- package/dist/operator-ui/model.d.ts.map +1 -0
- package/dist/operator-ui/model.js +28 -0
- package/dist/operator-ui/model.js.map +1 -0
- package/dist/registry.d.ts +33 -184
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +345 -330
- package/dist/registry.js.map +1 -1
- package/dist/retired-toolkits.d.ts +3 -0
- package/dist/retired-toolkits.d.ts.map +1 -0
- package/dist/retired-toolkits.js +9 -0
- package/dist/retired-toolkits.js.map +1 -0
- package/dist/routes/activity.d.ts +3 -0
- package/dist/routes/activity.d.ts.map +1 -0
- package/dist/routes/activity.js +170 -0
- package/dist/routes/activity.js.map +1 -0
- package/dist/routes/credentials.d.ts +3 -0
- package/dist/routes/credentials.d.ts.map +1 -0
- package/dist/routes/credentials.js +170 -0
- package/dist/routes/credentials.js.map +1 -0
- package/dist/routes/mcp.d.ts +8 -0
- package/dist/routes/mcp.d.ts.map +1 -0
- package/dist/routes/mcp.js +270 -0
- package/dist/routes/mcp.js.map +1 -0
- package/dist/routes/oauth.d.ts +4 -0
- package/dist/routes/oauth.d.ts.map +1 -0
- package/dist/routes/oauth.js +263 -0
- package/dist/routes/oauth.js.map +1 -0
- package/dist/routes/shared.d.ts +76 -0
- package/dist/routes/shared.d.ts.map +1 -0
- package/dist/routes/shared.js +140 -0
- package/dist/routes/shared.js.map +1 -0
- package/dist/routes/ui.d.ts +3 -0
- package/dist/routes/ui.d.ts.map +1 -0
- package/dist/routes/ui.js +116 -0
- package/dist/routes/ui.js.map +1 -0
- package/dist/server.d.ts +9 -40
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +62 -1322
- package/dist/server.js.map +1 -1
- package/dist/skills.d.ts +3 -21
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +15 -43
- package/dist/skills.js.map +1 -1
- package/dist/storage/file.d.ts.map +1 -1
- package/dist/storage/file.js +3 -1
- package/dist/storage/file.js.map +1 -1
- package/dist/storage/memory.d.ts.map +1 -1
- package/dist/storage/memory.js +3 -1
- package/dist/storage/memory.js.map +1 -1
- package/dist/timeout.d.ts.map +1 -1
- package/dist/timeout.js +2 -4
- package/dist/timeout.js.map +1 -1
- package/dist/types.d.ts +9 -59
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +3 -94
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +30 -1427
- package/dist/ui.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +15 -8
- package/src/activity.ts +0 -9
- package/src/auth/bearer.ts +4 -33
- package/src/auth/clerk.ts +35 -48
- package/src/call-admission.ts +8 -6
- package/src/catalog-fingerprint.ts +52 -0
- package/src/catalog-limits.ts +11 -0
- package/src/catalog-service.ts +601 -0
- package/src/catalog.ts +57 -2
- package/src/concurrency.ts +39 -0
- package/src/connectors/api.ts +22 -12
- package/src/connectors/remote-mcp.ts +63 -45
- package/src/credentials.ts +2 -2
- package/src/errors.ts +21 -1
- package/src/execute.ts +193 -423
- package/src/executor-result.ts +3 -3
- package/src/executors/quickjs-child.ts +8 -4
- package/src/executors/quickjs-protocol.ts +1 -1
- package/src/executors/quickjs-runtime.ts +5 -1
- package/src/executors/quickjs.ts +11 -8
- package/src/favicon.ts +2 -2
- package/src/index.ts +70 -222
- package/src/invocation.ts +580 -0
- package/src/meta-tools.ts +393 -985
- package/src/operator-ui/browser.css +360 -0
- package/src/operator-ui/browser.ts +987 -0
- package/src/operator-ui/generated.ts +4 -0
- package/src/operator-ui/model.ts +105 -0
- package/src/registry.ts +459 -439
- package/src/retired-toolkits.ts +14 -0
- package/src/routes/activity.ts +215 -0
- package/src/routes/credentials.ts +254 -0
- package/src/routes/mcp.ts +339 -0
- package/src/routes/oauth.ts +322 -0
- package/src/routes/shared.ts +240 -0
- package/src/routes/ui.ts +143 -0
- package/src/server.ts +64 -1731
- package/src/skills.ts +15 -43
- package/src/storage/file.ts +3 -1
- package/src/storage/memory.ts +3 -1
- package/src/timeout.ts +2 -4
- package/src/types.ts +8 -60
- package/src/ui.ts +60 -1538
- package/src/version.ts +1 -1
- package/dist/credential-health.d.ts +0 -227
- package/dist/credential-health.d.ts.map +0 -1
- package/dist/credential-health.js +0 -606
- package/dist/credential-health.js.map +0 -1
- package/dist/toolkits.d.ts +0 -146
- package/dist/toolkits.d.ts.map +0 -1
- package/dist/toolkits.js +0 -322
- package/dist/toolkits.js.map +0 -1
- package/src/credential-health.ts +0 -832
- 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
|
+
}
|
package/src/routes/ui.ts
ADDED
|
@@ -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
|
+
}
|