@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
package/src/ui.ts
CHANGED
|
@@ -8,11 +8,35 @@ import {
|
|
|
8
8
|
closeConnectorScope,
|
|
9
9
|
type DeferredWork,
|
|
10
10
|
} from "./connector-scope.js";
|
|
11
|
+
import {
|
|
12
|
+
mapSettledWithConcurrency,
|
|
13
|
+
resolveDiscoveryConcurrency,
|
|
14
|
+
} from "./concurrency.js";
|
|
15
|
+
import {
|
|
16
|
+
type CredentialManagementCapability,
|
|
17
|
+
type UiConnector,
|
|
18
|
+
type UiData,
|
|
19
|
+
type UiTool,
|
|
20
|
+
} from "./operator-ui/model.js";
|
|
21
|
+
import {
|
|
22
|
+
OPERATOR_UI_CSS,
|
|
23
|
+
OPERATOR_UI_SCRIPT,
|
|
24
|
+
} from "./operator-ui/generated.js";
|
|
11
25
|
import type { Registry } from "./registry.js";
|
|
12
|
-
import type {
|
|
13
|
-
|
|
26
|
+
import type {
|
|
27
|
+
ConnectaBranding,
|
|
28
|
+
ConnectorStatus,
|
|
29
|
+
UiAuthConfig,
|
|
30
|
+
} from "./types.js";
|
|
14
31
|
import { CONNECTA_VERSION } from "./version.js";
|
|
15
32
|
|
|
33
|
+
export {
|
|
34
|
+
filterUiConnectors,
|
|
35
|
+
type CredentialManagementCapability,
|
|
36
|
+
type UiConnector,
|
|
37
|
+
type UiData,
|
|
38
|
+
} from "./operator-ui/model.js";
|
|
39
|
+
|
|
16
40
|
/** Connecta's default monochrome "C" mark. */
|
|
17
41
|
export const CONNECTA_FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
18
42
|
<style>
|
|
@@ -35,7 +59,7 @@ interface ResolvedBranding {
|
|
|
35
59
|
themeColor: string;
|
|
36
60
|
}
|
|
37
61
|
|
|
38
|
-
|
|
62
|
+
const DEFAULT_FAVICON_HREF = "/favicon.svg";
|
|
39
63
|
|
|
40
64
|
/**
|
|
41
65
|
* Branding arrives from operator config, which is untyped at a JS call site, so
|
|
@@ -91,6 +115,14 @@ function isSetUrlValue(value: unknown): boolean {
|
|
|
91
115
|
: value !== undefined && value !== null;
|
|
92
116
|
}
|
|
93
117
|
|
|
118
|
+
/**
|
|
119
|
+
* A JS string literal safe to inline in a script element. Escaping `/` keeps
|
|
120
|
+
* an operator-supplied `</script>` from terminating the element early.
|
|
121
|
+
*/
|
|
122
|
+
function stringForInlineScript(value: string): string {
|
|
123
|
+
return JSON.stringify(value).replace(/\//g, "\\/");
|
|
124
|
+
}
|
|
125
|
+
|
|
94
126
|
/**
|
|
95
127
|
* Names of the branding URLs the operator set that failed their gate and were
|
|
96
128
|
* replaced by a default. Lives beside the gates so the startup warning cannot
|
|
@@ -115,15 +147,6 @@ export function droppedBrandingUrls(branding?: ConnectaBranding): string[] {
|
|
|
115
147
|
];
|
|
116
148
|
}
|
|
117
149
|
|
|
118
|
-
/**
|
|
119
|
-
* A JS string literal safe to inline in a <script> block. JSON.stringify alone
|
|
120
|
-
* leaves `/` untouched, so a value containing "</script>" would close the
|
|
121
|
-
* element early — operator-supplied branding still goes through here.
|
|
122
|
-
*/
|
|
123
|
-
function escapeScriptString(value: string): string {
|
|
124
|
-
return JSON.stringify(value).replace(/\//g, "\\/");
|
|
125
|
-
}
|
|
126
|
-
|
|
127
150
|
/**
|
|
128
151
|
* True only for absolute `http:`/`https:` URLs. Downstream connectors control
|
|
129
152
|
* their `authorizationUrl`, so a hostile/misconfigured one could hand back a
|
|
@@ -242,92 +265,6 @@ export function droppedUiAuthUrls(uiAuth?: UiAuthConfig): string[] {
|
|
|
242
265
|
];
|
|
243
266
|
}
|
|
244
267
|
|
|
245
|
-
export interface UiTool {
|
|
246
|
-
name: string;
|
|
247
|
-
address: string;
|
|
248
|
-
description?: string;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
export interface UiConnector {
|
|
252
|
-
id: string;
|
|
253
|
-
title?: string;
|
|
254
|
-
description?: string;
|
|
255
|
-
status: "ok" | "auth_required" | "error";
|
|
256
|
-
message?: string;
|
|
257
|
-
authorizationUrl?: string;
|
|
258
|
-
toolCount: number;
|
|
259
|
-
tools: UiTool[];
|
|
260
|
-
/** This connector exposes operator-managed downstream OAuth lifecycle hooks. */
|
|
261
|
-
oauth?: boolean;
|
|
262
|
-
/**
|
|
263
|
-
* Verdict of the last proactive credential liveness check (issue #24), for the
|
|
264
|
-
* connectors that hold a credential connecta stores. Shown beside the live
|
|
265
|
-
* status so an operator can tell "checked just now" from "last verified an
|
|
266
|
-
* hour ago", and see a dead credential the page's own probe may not reach.
|
|
267
|
-
*/
|
|
268
|
-
credentialCheck?: {
|
|
269
|
-
state: "ok" | "auth_required" | "error";
|
|
270
|
-
checkedAt: string;
|
|
271
|
-
message?: string;
|
|
272
|
-
};
|
|
273
|
-
credential?: {
|
|
274
|
-
label: string;
|
|
275
|
-
description?: string;
|
|
276
|
-
placeholder?: string;
|
|
277
|
-
fields?: Array<{
|
|
278
|
-
name: string;
|
|
279
|
-
label: string;
|
|
280
|
-
description?: string;
|
|
281
|
-
placeholder?: string;
|
|
282
|
-
inputType: "email" | "password" | "text";
|
|
283
|
-
configured: boolean;
|
|
284
|
-
lastFour?: string;
|
|
285
|
-
updatedAt?: string;
|
|
286
|
-
}>;
|
|
287
|
-
configured: boolean;
|
|
288
|
-
/** A stored value exists and may be deleted even if it cannot be decrypted. */
|
|
289
|
-
removable?: boolean;
|
|
290
|
-
lastFour?: string;
|
|
291
|
-
updatedAt?: string;
|
|
292
|
-
testable: boolean;
|
|
293
|
-
error?: string;
|
|
294
|
-
/**
|
|
295
|
-
* Something true and non-blocking about a working credential — today, that
|
|
296
|
-
* the vault still holds fields the connector has stopped declaring.
|
|
297
|
-
* Distinct from `error`, which means the credential cannot be used.
|
|
298
|
-
*/
|
|
299
|
-
notice?: string;
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
export interface UiData {
|
|
304
|
-
serverInfo: { name: string; version: string };
|
|
305
|
-
/** Version of the installed @zackbart/connecta package. */
|
|
306
|
-
connectaVersion: string;
|
|
307
|
-
connectors: UiConnector[];
|
|
308
|
-
/** Read-only projection of the validated deployment config. */
|
|
309
|
-
toolkits: UiToolkit[];
|
|
310
|
-
activityEnabled: boolean;
|
|
311
|
-
credentialManagement: CredentialManagementCapability;
|
|
312
|
-
/** True only for an eligible, unrestricted Clerk operator. */
|
|
313
|
-
oauthManagement: boolean;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
export interface UiToolkit {
|
|
317
|
-
name: string;
|
|
318
|
-
connectors: string[];
|
|
319
|
-
includeTools: string[];
|
|
320
|
-
excludeTools: string[];
|
|
321
|
-
/** Tools currently loaded through healthy connectors and visible in this view. */
|
|
322
|
-
toolCount: number;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
export type CredentialManagementCapability =
|
|
326
|
-
| "available"
|
|
327
|
-
| "requires_clerk"
|
|
328
|
-
| "vault_not_configured"
|
|
329
|
-
| "no_slots";
|
|
330
|
-
|
|
331
268
|
export type OperatorPage = "connections" | "credentials" | "activity";
|
|
332
269
|
|
|
333
270
|
const OPERATOR_PAGE_LABELS: Readonly<Record<OperatorPage, string>> = {
|
|
@@ -361,44 +298,6 @@ export function credentialManagementCapability(input: {
|
|
|
361
298
|
return "available";
|
|
362
299
|
}
|
|
363
300
|
|
|
364
|
-
export interface FilteredUiConnector {
|
|
365
|
-
connector: UiConnector;
|
|
366
|
-
tools: UiTool[];
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Filter by connector identity/description or tool name/description. A
|
|
371
|
-
* connector-level match stays visible even when it currently exposes no tools
|
|
372
|
-
* (for example while authorization is required).
|
|
373
|
-
*/
|
|
374
|
-
export function filterUiConnectors(
|
|
375
|
-
connectors: UiConnector[],
|
|
376
|
-
query: string,
|
|
377
|
-
): FilteredUiConnector[] {
|
|
378
|
-
const q = query.trim().toLowerCase();
|
|
379
|
-
const filtered: FilteredUiConnector[] = [];
|
|
380
|
-
for (const connector of connectors) {
|
|
381
|
-
const connectorText = [
|
|
382
|
-
connector.id,
|
|
383
|
-
connector.title,
|
|
384
|
-
connector.description,
|
|
385
|
-
connector.status,
|
|
386
|
-
]
|
|
387
|
-
.join(" ")
|
|
388
|
-
.toLowerCase();
|
|
389
|
-
const connectorMatches = Boolean(q && connectorText.includes(q));
|
|
390
|
-
const tools = connector.tools.filter(
|
|
391
|
-
(tool) =>
|
|
392
|
-
!q ||
|
|
393
|
-
connectorMatches ||
|
|
394
|
-
`${tool.name} ${tool.description ?? ""}`.toLowerCase().includes(q),
|
|
395
|
-
);
|
|
396
|
-
if (q && tools.length === 0 && !connectorMatches) continue;
|
|
397
|
-
filtered.push({ connector, tools });
|
|
398
|
-
}
|
|
399
|
-
return filtered;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
301
|
/**
|
|
403
302
|
* Build the read-only status payload served at `/ui/data`. Broken connectors are
|
|
404
303
|
* isolated: they surface status "error" with an empty tool list rather than
|
|
@@ -413,16 +312,21 @@ export async function buildUiData(
|
|
|
413
312
|
credentialManagement: CredentialManagementCapability = credentialVault
|
|
414
313
|
? "available"
|
|
415
314
|
: "requires_clerk",
|
|
416
|
-
toolkits?: ReadonlyMap<string, Toolkit>,
|
|
417
315
|
defer?: DeferredWork,
|
|
418
316
|
oauthManagement = false,
|
|
317
|
+
discoveryConcurrency?: number,
|
|
419
318
|
): Promise<UiData> {
|
|
420
319
|
const requestScope = {};
|
|
421
320
|
const connectorSet = registry.listConnectors();
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
321
|
+
const concurrency = resolveDiscoveryConcurrency(discoveryConcurrency);
|
|
322
|
+
const settled = await mapSettledWithConcurrency(
|
|
323
|
+
connectorSet,
|
|
324
|
+
concurrency,
|
|
325
|
+
async (c): Promise<UiConnector> => {
|
|
326
|
+
const drift = await registry.credentialDriftFor(c.id);
|
|
327
|
+
const status: ConnectorStatus = drift
|
|
328
|
+
? { state: "auth_required", message: drift }
|
|
329
|
+
: await registry.statusFor(c.id, baseUrl, requestScope);
|
|
426
330
|
let tools: UiTool[] = [];
|
|
427
331
|
// `status()` on an unauthenticated remote connector starts OAuth and
|
|
428
332
|
// stores its state + PKCE verifier. Probing listTools immediately
|
|
@@ -436,7 +340,9 @@ export async function buildUiData(
|
|
|
436
340
|
).map((t) => ({
|
|
437
341
|
name: t.name,
|
|
438
342
|
address: `${c.id}.${t.name}`,
|
|
439
|
-
|
|
343
|
+
...(t.description !== undefined
|
|
344
|
+
? { description: t.description }
|
|
345
|
+
: {}),
|
|
440
346
|
}));
|
|
441
347
|
} catch {
|
|
442
348
|
// broken connector: reported via status "error", tools stay empty
|
|
@@ -533,7 +439,9 @@ export async function buildUiData(
|
|
|
533
439
|
return {
|
|
534
440
|
id: c.id,
|
|
535
441
|
...(c.title ? { title: c.title } : {}),
|
|
536
|
-
|
|
442
|
+
...(c.description !== undefined
|
|
443
|
+
? { description: c.description }
|
|
444
|
+
: {}),
|
|
537
445
|
status: status.state,
|
|
538
446
|
...(status.message ? { message: status.message } : {}),
|
|
539
447
|
...(isSafeHttpUrl(status.authorizationUrl)
|
|
@@ -542,57 +450,28 @@ export async function buildUiData(
|
|
|
542
450
|
toolCount: tools.length,
|
|
543
451
|
tools,
|
|
544
452
|
...(c.disconnectAuth && c.startAuth ? { oauth: true } : {}),
|
|
545
|
-
...(credentialCheck
|
|
546
|
-
? {
|
|
547
|
-
credentialCheck: {
|
|
548
|
-
state: credentialCheck.state,
|
|
549
|
-
checkedAt: credentialCheck.checkedAt,
|
|
550
|
-
...(credentialCheck.message
|
|
551
|
-
? { message: credentialCheck.message }
|
|
552
|
-
: {}),
|
|
553
|
-
},
|
|
554
|
-
}
|
|
555
|
-
: {}),
|
|
556
453
|
...(credential ? { credential } : {}),
|
|
557
454
|
};
|
|
558
|
-
}
|
|
455
|
+
},
|
|
559
456
|
);
|
|
560
|
-
await
|
|
561
|
-
connectorSet
|
|
457
|
+
await mapSettledWithConcurrency(
|
|
458
|
+
connectorSet,
|
|
459
|
+
concurrency,
|
|
460
|
+
(connector) =>
|
|
562
461
|
closeConnectorScope(
|
|
563
462
|
connector,
|
|
564
463
|
registry.contextFor(connector.id, baseUrl, requestScope),
|
|
565
464
|
defer,
|
|
566
465
|
),
|
|
567
|
-
),
|
|
568
466
|
);
|
|
569
467
|
const connectors = settled.map((result) => {
|
|
570
468
|
if (result.status === "rejected") throw result.reason;
|
|
571
469
|
return result.value;
|
|
572
470
|
});
|
|
573
|
-
const toolkitData: UiToolkit[] = [...(toolkits?.values() ?? [])].map(
|
|
574
|
-
(toolkit) => ({
|
|
575
|
-
name: toolkit.name,
|
|
576
|
-
connectors: [...toolkit.connectors],
|
|
577
|
-
includeTools: [...toolkit.includeTools],
|
|
578
|
-
excludeTools: [...toolkit.excludeTools],
|
|
579
|
-
toolCount: connectors.reduce(
|
|
580
|
-
(count, connector) =>
|
|
581
|
-
count +
|
|
582
|
-
(toolkit.hasConnector(connector.id)
|
|
583
|
-
? connector.tools.filter((tool) =>
|
|
584
|
-
toolkit.hasTool(connector.id, tool.name),
|
|
585
|
-
).length
|
|
586
|
-
: 0),
|
|
587
|
-
0,
|
|
588
|
-
),
|
|
589
|
-
}),
|
|
590
|
-
);
|
|
591
471
|
return {
|
|
592
472
|
serverInfo,
|
|
593
473
|
connectaVersion: CONNECTA_VERSION,
|
|
594
474
|
connectors,
|
|
595
|
-
toolkits: toolkitData,
|
|
596
475
|
activityEnabled,
|
|
597
476
|
credentialManagement,
|
|
598
477
|
oauthManagement,
|
|
@@ -690,421 +569,7 @@ export function renderUiHtml(
|
|
|
690
569
|
<link rel="shortcut icon" href="/favicon.ico">
|
|
691
570
|
<title>${escapeHtmlAttr(title)}</title>
|
|
692
571
|
${clerkScript}
|
|
693
|
-
<style>
|
|
694
|
-
:root {
|
|
695
|
-
color-scheme: light;
|
|
696
|
-
--ink: #000;
|
|
697
|
-
--paper: #fff;
|
|
698
|
-
--rule: #ccc;
|
|
699
|
-
--muted: #666;
|
|
700
|
-
--trace: #f5f5f5;
|
|
701
|
-
--shell: 70rem;
|
|
702
|
-
--pad: 1rem;
|
|
703
|
-
--gap: 1.5rem;
|
|
704
|
-
--sans: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
705
|
-
--mono: ui-monospace, "SF Mono", Menlo, Monaco, "Cascadia Code", Consolas,
|
|
706
|
-
monospace;
|
|
707
|
-
}
|
|
708
|
-
* { border-radius: 0; box-sizing: border-box; }
|
|
709
|
-
html {
|
|
710
|
-
background: var(--paper);
|
|
711
|
-
color: var(--ink);
|
|
712
|
-
font-family: var(--sans);
|
|
713
|
-
font-size: 16px;
|
|
714
|
-
line-height: 1.5;
|
|
715
|
-
-webkit-font-smoothing: antialiased;
|
|
716
|
-
text-rendering: optimizeLegibility;
|
|
717
|
-
}
|
|
718
|
-
body { margin: 0; min-height: 100vh; }
|
|
719
|
-
::selection { background: var(--ink); color: var(--paper); }
|
|
720
|
-
:is(h1, h2, h3, p, ul, ol) { margin: 0; padding: 0; }
|
|
721
|
-
:is(h1, h2, h3) { font-size: inherit; font-weight: 400; }
|
|
722
|
-
:is(ul, ol) { list-style: none; }
|
|
723
|
-
a { color: inherit; }
|
|
724
|
-
button, input { font: inherit; }
|
|
725
|
-
button {
|
|
726
|
-
background: none;
|
|
727
|
-
border: 0;
|
|
728
|
-
color: inherit;
|
|
729
|
-
cursor: pointer;
|
|
730
|
-
margin: 0;
|
|
731
|
-
padding: 0;
|
|
732
|
-
text-align: left;
|
|
733
|
-
}
|
|
734
|
-
button:disabled { cursor: wait; opacity: .5; }
|
|
735
|
-
input {
|
|
736
|
-
background: var(--paper);
|
|
737
|
-
border: 1px solid var(--rule);
|
|
738
|
-
color: var(--ink);
|
|
739
|
-
min-height: 2rem;
|
|
740
|
-
padding: .2rem .5rem;
|
|
741
|
-
}
|
|
742
|
-
input:focus-visible { outline-offset: -1px; }
|
|
743
|
-
:is(a, button, input, summary):focus-visible {
|
|
744
|
-
outline: 1px solid var(--ink);
|
|
745
|
-
}
|
|
746
|
-
:is(a, button, summary):focus-visible { outline-offset: 2px; }
|
|
747
|
-
.skip-link {
|
|
748
|
-
background: var(--paper);
|
|
749
|
-
left: var(--pad);
|
|
750
|
-
padding: .5rem;
|
|
751
|
-
position: fixed;
|
|
752
|
-
top: -4rem;
|
|
753
|
-
z-index: 10;
|
|
754
|
-
}
|
|
755
|
-
.skip-link:focus { top: var(--pad); }
|
|
756
|
-
|
|
757
|
-
.shell {
|
|
758
|
-
margin: 0 auto;
|
|
759
|
-
max-width: var(--shell);
|
|
760
|
-
padding-left: var(--pad);
|
|
761
|
-
padding-right: var(--pad);
|
|
762
|
-
}
|
|
763
|
-
.pgrid {
|
|
764
|
-
column-gap: var(--gap);
|
|
765
|
-
display: grid;
|
|
766
|
-
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
767
|
-
row-gap: 1rem;
|
|
768
|
-
}
|
|
769
|
-
.pcap { grid-column: 1; }
|
|
770
|
-
.pbody { grid-column: 2 / -1; min-width: 0; }
|
|
771
|
-
.cap, .meta { color: var(--muted); font-size: .9em; }
|
|
772
|
-
.mono { font-family: var(--mono); font-size: .78rem; }
|
|
773
|
-
.hidden { display: none !important; }
|
|
774
|
-
.visually-hidden {
|
|
775
|
-
clip: rect(0 0 0 0);
|
|
776
|
-
clip-path: inset(50%);
|
|
777
|
-
height: 1px;
|
|
778
|
-
overflow: hidden;
|
|
779
|
-
position: absolute;
|
|
780
|
-
white-space: nowrap;
|
|
781
|
-
width: 1px;
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
.masthead {
|
|
785
|
-
align-items: start;
|
|
786
|
-
padding-bottom: var(--pad);
|
|
787
|
-
padding-top: var(--pad);
|
|
788
|
-
}
|
|
789
|
-
.brand {
|
|
790
|
-
font-weight: 500;
|
|
791
|
-
grid-column: 1;
|
|
792
|
-
text-decoration: none;
|
|
793
|
-
}
|
|
794
|
-
.mast-nav {
|
|
795
|
-
display: flex;
|
|
796
|
-
gap: var(--gap);
|
|
797
|
-
grid-column: 2 / -1;
|
|
798
|
-
justify-content: space-between;
|
|
799
|
-
min-width: 0;
|
|
800
|
-
}
|
|
801
|
-
.mast-actions {
|
|
802
|
-
display: flex;
|
|
803
|
-
gap: var(--gap);
|
|
804
|
-
justify-content: flex-end;
|
|
805
|
-
min-width: 0;
|
|
806
|
-
}
|
|
807
|
-
.page-nav,
|
|
808
|
-
.session-actions {
|
|
809
|
-
display: flex;
|
|
810
|
-
flex-wrap: wrap;
|
|
811
|
-
gap: .5rem var(--gap);
|
|
812
|
-
}
|
|
813
|
-
.mast-actions :is(a, button) {
|
|
814
|
-
align-items: center;
|
|
815
|
-
display: inline-flex;
|
|
816
|
-
min-height: 2rem;
|
|
817
|
-
}
|
|
818
|
-
.navlink,
|
|
819
|
-
.linklike {
|
|
820
|
-
text-decoration: underline;
|
|
821
|
-
text-decoration-thickness: 1.5px;
|
|
822
|
-
text-underline-offset: .22em;
|
|
823
|
-
}
|
|
824
|
-
.navlink { text-decoration-color: transparent; }
|
|
825
|
-
.navlink:hover,
|
|
826
|
-
.navlink:focus-visible,
|
|
827
|
-
.navlink[aria-current="page"] { text-decoration-color: currentColor; }
|
|
828
|
-
.linklike { text-decoration-color: currentColor; }
|
|
829
|
-
.linklike:hover,
|
|
830
|
-
.linklike:focus-visible { text-decoration-color: transparent; }
|
|
831
|
-
|
|
832
|
-
.page { padding-bottom: 5rem; }
|
|
833
|
-
.lead { margin-top: 6rem; }
|
|
834
|
-
.section,
|
|
835
|
-
.section + .section { margin-top: 3rem; }
|
|
836
|
-
.lead-copy,
|
|
837
|
-
.body-copy { max-width: 34em; }
|
|
838
|
-
.lead-copy > * + *,
|
|
839
|
-
.body-copy > * + * { margin-top: 1.5rem; }
|
|
840
|
-
.row,
|
|
841
|
-
.actions {
|
|
842
|
-
align-items: center;
|
|
843
|
-
display: flex;
|
|
844
|
-
flex-wrap: wrap;
|
|
845
|
-
gap: var(--gap);
|
|
846
|
-
}
|
|
847
|
-
.row input { flex: 1; min-width: 12rem; }
|
|
848
|
-
|
|
849
|
-
.gate-actions { margin-top: 1.5rem; }
|
|
850
|
-
#err { margin-top: 1.5rem; text-decoration: underline; }
|
|
851
|
-
|
|
852
|
-
.endpoint { border-bottom: 1px solid var(--rule); border-top: 1px solid var(--rule); }
|
|
853
|
-
.endpoint-row {
|
|
854
|
-
align-items: baseline;
|
|
855
|
-
display: flex;
|
|
856
|
-
gap: var(--gap);
|
|
857
|
-
min-width: 0;
|
|
858
|
-
padding: .75rem 0;
|
|
859
|
-
}
|
|
860
|
-
.endpoint-row code {
|
|
861
|
-
flex: 1;
|
|
862
|
-
min-width: 0;
|
|
863
|
-
overflow-x: auto;
|
|
864
|
-
white-space: nowrap;
|
|
865
|
-
}
|
|
866
|
-
.endpoint-row button { flex: none; }
|
|
867
|
-
|
|
868
|
-
.connector-tools { border-bottom: 1px solid var(--rule); }
|
|
869
|
-
.toolbar { margin-bottom: 1.5rem; }
|
|
870
|
-
.toolbar input { flex-basis: 18rem; }
|
|
871
|
-
#notice { margin-bottom: .75rem; }
|
|
872
|
-
#notice:empty { display: none; }
|
|
873
|
-
#notice:not(:empty) { text-decoration: underline; }
|
|
874
|
-
#oauthNotice { margin-bottom: .75rem; }
|
|
875
|
-
#oauthNotice:empty { display: none; }
|
|
876
|
-
.error-notice, .msg { text-decoration: underline; }
|
|
877
|
-
.card,
|
|
878
|
-
.credential-card,
|
|
879
|
-
.activity-item {
|
|
880
|
-
padding-left: 1.25rem;
|
|
881
|
-
position: relative;
|
|
882
|
-
}
|
|
883
|
-
.card::before,
|
|
884
|
-
.credential-card::before,
|
|
885
|
-
.activity-item::before {
|
|
886
|
-
background: var(--rule);
|
|
887
|
-
bottom: 0;
|
|
888
|
-
content: "";
|
|
889
|
-
left: .25rem;
|
|
890
|
-
position: absolute;
|
|
891
|
-
top: 0;
|
|
892
|
-
width: 1px;
|
|
893
|
-
}
|
|
894
|
-
.card { border-top: 1px solid var(--rule); padding-bottom: .75rem; padding-top: .75rem; }
|
|
895
|
-
.connector-head {
|
|
896
|
-
display: grid;
|
|
897
|
-
gap: var(--gap);
|
|
898
|
-
grid-template-columns: minmax(0, 2fr) minmax(10rem, 1fr);
|
|
899
|
-
}
|
|
900
|
-
.connector-title {
|
|
901
|
-
align-items: baseline;
|
|
902
|
-
display: flex;
|
|
903
|
-
gap: .5rem;
|
|
904
|
-
}
|
|
905
|
-
.connector-title .dot,
|
|
906
|
-
.activity-stamp .dot {
|
|
907
|
-
margin-left: -1.25rem;
|
|
908
|
-
}
|
|
909
|
-
.activity-stamp {
|
|
910
|
-
align-items: baseline;
|
|
911
|
-
display: flex;
|
|
912
|
-
gap: .75rem;
|
|
913
|
-
}
|
|
914
|
-
.card h2 { overflow-wrap: anywhere; }
|
|
915
|
-
.connector-state { text-align: right; }
|
|
916
|
-
.dot {
|
|
917
|
-
background: var(--paper);
|
|
918
|
-
border: 1px solid var(--ink);
|
|
919
|
-
display: inline-block;
|
|
920
|
-
flex: none;
|
|
921
|
-
height: .5rem;
|
|
922
|
-
width: .5rem;
|
|
923
|
-
z-index: 1;
|
|
924
|
-
}
|
|
925
|
-
.dot.ok { background: var(--ink); }
|
|
926
|
-
.dot.auth_required {
|
|
927
|
-
background: linear-gradient(90deg, var(--ink) 50%, var(--paper) 50%);
|
|
928
|
-
}
|
|
929
|
-
.connector-description { margin-top: .25rem; max-width: 40rem; }
|
|
930
|
-
.connector-message,
|
|
931
|
-
.connector-auth { margin-top: .75rem; }
|
|
932
|
-
.connector-toolkits {
|
|
933
|
-
align-items: baseline;
|
|
934
|
-
display: flex;
|
|
935
|
-
flex-wrap: wrap;
|
|
936
|
-
gap: .25rem .5rem;
|
|
937
|
-
margin-top: .5rem;
|
|
938
|
-
}
|
|
939
|
-
.scope-label {
|
|
940
|
-
border: 1px solid var(--rule);
|
|
941
|
-
display: inline-block;
|
|
942
|
-
font-family: var(--mono);
|
|
943
|
-
font-size: .72rem;
|
|
944
|
-
padding: .05rem .35rem;
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
.toolkit-copy { margin-bottom: 1rem; max-width: 42rem; }
|
|
948
|
-
.toolkit-ledger { border-bottom: 1px solid var(--rule); }
|
|
949
|
-
.toolkit-card {
|
|
950
|
-
border-top: 1px solid var(--rule);
|
|
951
|
-
padding: .9rem 0 1rem;
|
|
952
|
-
}
|
|
953
|
-
.toolkit-head {
|
|
954
|
-
display: grid;
|
|
955
|
-
gap: var(--gap);
|
|
956
|
-
grid-template-columns: minmax(0, 2fr) minmax(10rem, 1fr);
|
|
957
|
-
}
|
|
958
|
-
.toolkit-name {
|
|
959
|
-
align-items: baseline;
|
|
960
|
-
display: flex;
|
|
961
|
-
flex-wrap: wrap;
|
|
962
|
-
gap: .5rem;
|
|
963
|
-
}
|
|
964
|
-
.toolkit-name h3 { overflow-wrap: anywhere; }
|
|
965
|
-
.toolkit-state { text-align: right; }
|
|
966
|
-
.toolkit-connectors {
|
|
967
|
-
display: flex;
|
|
968
|
-
flex-wrap: wrap;
|
|
969
|
-
gap: .5rem;
|
|
970
|
-
margin-top: .75rem;
|
|
971
|
-
}
|
|
972
|
-
.toolkit-connector {
|
|
973
|
-
border-left: 1px solid var(--ink);
|
|
974
|
-
display: inline-flex;
|
|
975
|
-
flex-wrap: wrap;
|
|
976
|
-
gap: .4rem;
|
|
977
|
-
padding-left: .5rem;
|
|
978
|
-
}
|
|
979
|
-
.toolkit-rules { margin-top: .75rem; }
|
|
980
|
-
.toolkit-rules > * + * { margin-top: .25rem; }
|
|
981
|
-
.toolkit-endpoint { margin-top: .75rem; }
|
|
982
|
-
|
|
983
|
-
.credential-ledger { border-bottom: 1px solid var(--rule); }
|
|
984
|
-
.credential-card { border-top: 1px solid var(--rule); padding-bottom: .75rem; padding-top: .75rem; }
|
|
985
|
-
.credential-head {
|
|
986
|
-
align-items: baseline;
|
|
987
|
-
display: flex;
|
|
988
|
-
flex-wrap: wrap;
|
|
989
|
-
gap: .25rem var(--gap);
|
|
990
|
-
justify-content: space-between;
|
|
991
|
-
}
|
|
992
|
-
.credential-copy { margin-top: .25rem; max-width: 40rem; }
|
|
993
|
-
.credential-field-summary {
|
|
994
|
-
border-top: 1px solid var(--rule);
|
|
995
|
-
margin-top: .75rem;
|
|
996
|
-
}
|
|
997
|
-
.credential-field-summary > div {
|
|
998
|
-
border-bottom: 1px solid var(--rule);
|
|
999
|
-
display: flex;
|
|
1000
|
-
flex-wrap: wrap;
|
|
1001
|
-
gap: .25rem var(--gap);
|
|
1002
|
-
justify-content: space-between;
|
|
1003
|
-
padding: .5rem 0;
|
|
1004
|
-
}
|
|
1005
|
-
.credential-actions { display: flex; flex-wrap: wrap; gap: var(--gap); margin-top: .75rem; }
|
|
1006
|
-
.credential-actions button,
|
|
1007
|
-
.credential-form button,
|
|
1008
|
-
.activity-controls button,
|
|
1009
|
-
.activity-more {
|
|
1010
|
-
align-items: center;
|
|
1011
|
-
display: inline-flex;
|
|
1012
|
-
min-height: 2.75rem;
|
|
1013
|
-
}
|
|
1014
|
-
.credential-form {
|
|
1015
|
-
align-items: center;
|
|
1016
|
-
display: flex;
|
|
1017
|
-
flex-wrap: wrap;
|
|
1018
|
-
gap: .75rem var(--gap);
|
|
1019
|
-
margin-top: .75rem;
|
|
1020
|
-
}
|
|
1021
|
-
.credential-form > input { flex: 1 1 18rem; }
|
|
1022
|
-
.credential-fields { display: grid; flex: 1 1 100%; gap: .75rem; }
|
|
1023
|
-
.credential-field {
|
|
1024
|
-
align-items: center;
|
|
1025
|
-
display: grid;
|
|
1026
|
-
gap: var(--gap);
|
|
1027
|
-
grid-template-columns: minmax(9rem, 12rem) 1fr;
|
|
1028
|
-
}
|
|
1029
|
-
.credential-field input { min-width: 0; width: 100%; }
|
|
1030
|
-
.danger { text-decoration-style: double; }
|
|
1031
|
-
|
|
1032
|
-
details { margin-top: .75rem; }
|
|
1033
|
-
summary { cursor: pointer; list-style: none; width: max-content; }
|
|
1034
|
-
summary::-webkit-details-marker { display: none; }
|
|
1035
|
-
.tool-list { border-bottom: 1px solid var(--rule); margin-top: .5rem; }
|
|
1036
|
-
.tool {
|
|
1037
|
-
border-top: 1px solid var(--rule);
|
|
1038
|
-
display: grid;
|
|
1039
|
-
gap: .25rem var(--gap);
|
|
1040
|
-
grid-template-columns: minmax(12rem, 1fr) minmax(0, 2fr);
|
|
1041
|
-
padding: .5rem 0;
|
|
1042
|
-
}
|
|
1043
|
-
.tool code { font-family: var(--mono); font-size: .78rem; overflow-wrap: anywhere; }
|
|
1044
|
-
.tool .td { color: var(--muted); font-size: .9em; }
|
|
1045
|
-
.empty { border-top: 1px solid var(--rule); padding: .75rem 0; }
|
|
1046
|
-
|
|
1047
|
-
.activity-copy { margin-bottom: 1.5rem; }
|
|
1048
|
-
.activity-controls { margin-bottom: 1.5rem; }
|
|
1049
|
-
.activity-controls input { flex: 1 1 18rem; }
|
|
1050
|
-
#activityNotice { margin-bottom: .75rem; }
|
|
1051
|
-
.activity-ledger { border-bottom: 1px solid var(--rule); }
|
|
1052
|
-
.activity-item {
|
|
1053
|
-
border-top: 1px solid var(--rule);
|
|
1054
|
-
display: grid;
|
|
1055
|
-
gap: .25rem var(--gap);
|
|
1056
|
-
grid-template-columns: minmax(9rem, .85fr) minmax(12rem, 1.4fr) minmax(8rem, .9fr);
|
|
1057
|
-
padding-bottom: .75rem;
|
|
1058
|
-
padding-top: .75rem;
|
|
1059
|
-
}
|
|
1060
|
-
.activity-time,
|
|
1061
|
-
.activity-actor,
|
|
1062
|
-
.activity-detail { color: var(--muted); font-size: .82rem; }
|
|
1063
|
-
.activity-actor-id { color: var(--muted); margin-top: .1rem; }
|
|
1064
|
-
.activity-address { font-family: var(--mono); font-size: .78rem; overflow-wrap: anywhere; }
|
|
1065
|
-
.activity-outcome { font-size: .9em; }
|
|
1066
|
-
.activity-item.error .activity-outcome,
|
|
1067
|
-
.activity-item.timeout .activity-outcome,
|
|
1068
|
-
.activity-item.cancelled .activity-outcome { text-decoration: underline; }
|
|
1069
|
-
.activity-empty { border-top: 1px solid var(--rule); padding: .75rem 0; }
|
|
1070
|
-
.activity-more { margin-top: .75rem; }
|
|
1071
|
-
.unavailable {
|
|
1072
|
-
background: var(--trace);
|
|
1073
|
-
border-bottom: 1px solid var(--rule);
|
|
1074
|
-
border-top: 1px solid var(--rule);
|
|
1075
|
-
padding: .75rem;
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
@media (prefers-reduced-motion: reduce) {
|
|
1079
|
-
html:focus-within { scroll-behavior: auto; }
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
@media (max-width: 36.99rem) {
|
|
1083
|
-
.pgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
1084
|
-
.pcap,
|
|
1085
|
-
.pbody { grid-column: 1 / -1; }
|
|
1086
|
-
.masthead .brand { grid-column: 1; }
|
|
1087
|
-
.mast-nav { grid-column: 1 / -1; grid-row: 2; justify-content: flex-start; }
|
|
1088
|
-
.product { display: none; }
|
|
1089
|
-
.mast-actions {
|
|
1090
|
-
align-items: flex-start;
|
|
1091
|
-
flex-direction: column;
|
|
1092
|
-
font-size: .875rem;
|
|
1093
|
-
gap: .25rem;
|
|
1094
|
-
}
|
|
1095
|
-
.lead { margin-top: 4rem; }
|
|
1096
|
-
.section,
|
|
1097
|
-
.section + .section { margin-top: 2.5rem; }
|
|
1098
|
-
.connector-head,
|
|
1099
|
-
.toolkit-head,
|
|
1100
|
-
.tool,
|
|
1101
|
-
.activity-item { grid-template-columns: 1fr; }
|
|
1102
|
-
.connector-state,
|
|
1103
|
-
.toolkit-state { text-align: left; }
|
|
1104
|
-
.credential-field { align-items: start; grid-template-columns: 1fr; gap: .25rem; }
|
|
1105
|
-
input { min-height: 2.75rem; }
|
|
1106
|
-
}
|
|
1107
|
-
</style>
|
|
572
|
+
<style>${OPERATOR_UI_CSS}</style>
|
|
1108
573
|
</head>
|
|
1109
574
|
<body>
|
|
1110
575
|
<a class="skip-link" href="#operatorContent">Skip to operator page</a>
|
|
@@ -1176,16 +641,6 @@ ${clerkScript}
|
|
|
1176
641
|
<div id="list" class="connector-tools" aria-busy="false"></div>
|
|
1177
642
|
</div>
|
|
1178
643
|
</section>
|
|
1179
|
-
<section class="section pgrid" aria-labelledby="toolkitLedgerHeading">
|
|
1180
|
-
<h2 class="pcap" id="toolkitLedgerHeading">Toolkits</h2>
|
|
1181
|
-
<div class="pbody">
|
|
1182
|
-
<p class="toolkit-copy meta">
|
|
1183
|
-
Read-only views from deployment config. Change the config and redeploy
|
|
1184
|
-
to update them.
|
|
1185
|
-
</p>
|
|
1186
|
-
<div id="toolkitList" class="toolkit-ledger"></div>
|
|
1187
|
-
</div>
|
|
1188
|
-
</section>
|
|
1189
644
|
</section>
|
|
1190
645
|
|
|
1191
646
|
<section id="credentialsView"${page === "credentials" ? "" : ' class="hidden"'}>
|
|
@@ -1232,942 +687,9 @@ const AUTH = ${jsonForInlineScript(auth)};
|
|
|
1232
687
|
const MCP_URL = ${jsonForInlineScript(mcpUrl)};
|
|
1233
688
|
const INITIAL_PAGE = ${jsonForInlineScript(page)};
|
|
1234
689
|
const TITLE_SUFFIX = ${jsonForInlineScript(brand.pageTitle)};
|
|
1235
|
-
const
|
|
1236
|
-
const
|
|
1237
|
-
|
|
1238
|
-
const PAGE_META = {
|
|
1239
|
-
connections: { path: "/", label: "Connections" },
|
|
1240
|
-
credentials: { path: "/credentials", label: "Credentials" },
|
|
1241
|
-
activity: { path: "/activity", label: "Activity" },
|
|
1242
|
-
};
|
|
1243
|
-
let DATA = null;
|
|
1244
|
-
let ACTIVITY = [];
|
|
1245
|
-
let ACTIVITY_CURSOR = null;
|
|
1246
|
-
let ACTIVITY_LOADED = false;
|
|
1247
|
-
let CURRENT_PAGE = INITIAL_PAGE;
|
|
1248
|
-
let SESSION_GENERATION = 0;
|
|
1249
|
-
let ACTIVITY_GENERATION = 0;
|
|
1250
|
-
let CLERK_SESSION_ID = null;
|
|
1251
|
-
$("mcpUrl").textContent = MCP_URL;
|
|
1252
|
-
|
|
1253
|
-
function esc(s) {
|
|
1254
|
-
return String(s == null ? "" : s).replace(/[&<>"]/g, (c) =>
|
|
1255
|
-
({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
// Only http/https may become a clickable href — mirrors the server-side gate
|
|
1259
|
-
// so a hostile scheme (javascript:, data:) can never be linked. Defense in depth.
|
|
1260
|
-
function safeHttp(u) {
|
|
1261
|
-
try {
|
|
1262
|
-
const p = new URL(u).protocol;
|
|
1263
|
-
return p === "http:" || p === "https:" ? u : null;
|
|
1264
|
-
} catch (e) { return null; }
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
function formatDate(value) {
|
|
1268
|
-
if (!value) return "";
|
|
1269
|
-
const date = new Date(value);
|
|
1270
|
-
return Number.isNaN(date.valueOf()) ? "" : date.toLocaleString();
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
function setNotice(message, isError) {
|
|
1274
|
-
$("credentialNotice").textContent = message || "";
|
|
1275
|
-
$("credentialNotice").classList.toggle("error-notice", Boolean(isError));
|
|
1276
|
-
$("credentialNotice").setAttribute("role", isError ? "alert" : "status");
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
function setOauthNotice(message, isError) {
|
|
1280
|
-
$("oauthNotice").textContent = message || "";
|
|
1281
|
-
$("oauthNotice").classList.toggle("error-notice", Boolean(isError));
|
|
1282
|
-
$("oauthNotice").setAttribute("role", isError ? "alert" : "status");
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
async function sessionToken() {
|
|
1286
|
-
return AUTH.kind === "clerk"
|
|
1287
|
-
? await Clerk.session?.getToken()
|
|
1288
|
-
: localStorage.getItem(KEY);
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
function clearActivityState() {
|
|
1292
|
-
ACTIVITY_GENERATION += 1;
|
|
1293
|
-
ACTIVITY = [];
|
|
1294
|
-
ACTIVITY_CURSOR = null;
|
|
1295
|
-
ACTIVITY_LOADED = false;
|
|
1296
|
-
$("activityList").innerHTML = "";
|
|
1297
|
-
$("activityList").setAttribute("aria-busy", "false");
|
|
1298
|
-
$("activityNotice").textContent = "";
|
|
1299
|
-
$("activityNotice").setAttribute("role", "status");
|
|
1300
|
-
$("activitySummary").textContent = "Arguments and results are never stored.";
|
|
1301
|
-
$("activitySearch").value = "";
|
|
1302
|
-
$("refreshActivity").disabled = false;
|
|
1303
|
-
$("moreActivity").disabled = false;
|
|
1304
|
-
$("moreActivity").classList.add("hidden");
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
function clearIdentityState() {
|
|
1308
|
-
SESSION_GENERATION += 1;
|
|
1309
|
-
DATA = null;
|
|
1310
|
-
clearActivityState();
|
|
1311
|
-
$("list").innerHTML = "";
|
|
1312
|
-
setOauthNotice("");
|
|
1313
|
-
$("toolkitList").innerHTML = "";
|
|
1314
|
-
$("filter").value = "";
|
|
1315
|
-
$("credentialList").innerHTML = "";
|
|
1316
|
-
$("credentialList").setAttribute("aria-busy", "false");
|
|
1317
|
-
$("credentialNotice").textContent = "";
|
|
1318
|
-
$("credentialNotice").setAttribute("role", "status");
|
|
1319
|
-
$("credentialNotice").classList.remove("error-notice");
|
|
1320
|
-
$("credentialUnavailable").textContent = "";
|
|
1321
|
-
$("credentialUnavailable").classList.add("hidden");
|
|
1322
|
-
$("credentialList").classList.add("hidden");
|
|
1323
|
-
$("activityUnavailable").classList.add("hidden");
|
|
1324
|
-
$("activityAvailable").classList.add("hidden");
|
|
1325
|
-
$("credentialsNav").classList.add("hidden");
|
|
1326
|
-
$("activityNav").classList.add("hidden");
|
|
1327
|
-
$("serverInfo").textContent = ${escapeScriptString(brand.productName + " operator")};
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
function showGate(msg) {
|
|
1331
|
-
clearIdentityState();
|
|
1332
|
-
$("app").classList.add("hidden");
|
|
1333
|
-
$("appNav").classList.add("hidden");
|
|
1334
|
-
$("gate").classList.remove("hidden");
|
|
1335
|
-
$("err").textContent = msg || "";
|
|
1336
|
-
if (AUTH.kind === "clerk") {
|
|
1337
|
-
const signedIn = Boolean(window.Clerk && Clerk.user);
|
|
1338
|
-
$("gateCopy").textContent = signedIn
|
|
1339
|
-
? "Signed in with Clerk, but this account cannot open deployment-wide operator pages."
|
|
1340
|
-
: "Sign in with Clerk to open this operator page.";
|
|
1341
|
-
$("signin").classList.toggle("hidden", signedIn);
|
|
1342
|
-
$("gateSignout").classList.toggle("hidden", !signedIn);
|
|
1343
|
-
} else {
|
|
1344
|
-
$("gateCopy").textContent = "";
|
|
1345
|
-
}
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
function pageForPath(path) {
|
|
1349
|
-
if (path === "/credentials") return "credentials";
|
|
1350
|
-
if (path === "/activity") return "activity";
|
|
1351
|
-
return "connections";
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
function credentialUnavailableCopy(capability) {
|
|
1355
|
-
if (capability === "no_slots") {
|
|
1356
|
-
return "No connectors declare operator-managed credential slots. Connector credentials remain configuration-as-code until a slot is declared.";
|
|
1357
|
-
}
|
|
1358
|
-
if (capability === "vault_not_configured") {
|
|
1359
|
-
return "Credential storage is not configured. Set credentials.encryptionKey before managing connector credentials here.";
|
|
1360
|
-
}
|
|
1361
|
-
return "Credential management requires an eligible Clerk operator. Bearer-authenticated sessions can inspect connections but cannot manage stored credentials.";
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
function updateCapabilities() {
|
|
1365
|
-
const credentialsAvailable =
|
|
1366
|
-
DATA?.credentialManagement === "available";
|
|
1367
|
-
$("credentialsNav").classList.toggle("hidden", !credentialsAvailable);
|
|
1368
|
-
$("activityNav").classList.toggle("hidden", !DATA?.activityEnabled);
|
|
1369
|
-
$("credentialUnavailable").classList.toggle("hidden", credentialsAvailable);
|
|
1370
|
-
$("credentialList").classList.toggle("hidden", !credentialsAvailable);
|
|
1371
|
-
$("credentialUnavailable").textContent = credentialsAvailable
|
|
1372
|
-
? ""
|
|
1373
|
-
: credentialUnavailableCopy(DATA?.credentialManagement);
|
|
1374
|
-
$("activityUnavailable").classList.toggle("hidden", Boolean(DATA?.activityEnabled));
|
|
1375
|
-
$("activityAvailable").classList.toggle("hidden", !DATA?.activityEnabled);
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
function activatePage(page, options) {
|
|
1379
|
-
const next = PAGE_META[page] ? page : "connections";
|
|
1380
|
-
CURRENT_PAGE = next;
|
|
1381
|
-
for (const name of Object.keys(PAGE_META)) {
|
|
1382
|
-
$(name + "View").classList.toggle("hidden", name !== next);
|
|
1383
|
-
const link = $(name + "Nav");
|
|
1384
|
-
if (name === next) link.setAttribute("aria-current", "page");
|
|
1385
|
-
else link.removeAttribute("aria-current");
|
|
1386
|
-
}
|
|
1387
|
-
$("gateHeading").textContent = PAGE_META[next].label;
|
|
1388
|
-
document.title = PAGE_META[next].label + " — " + TITLE_SUFFIX;
|
|
1389
|
-
if (DATA) {
|
|
1390
|
-
updateCapabilities();
|
|
1391
|
-
if (next === "connections") renderConnections();
|
|
1392
|
-
if (next === "credentials") renderCredentials();
|
|
1393
|
-
if (next === "activity" && DATA.activityEnabled && !ACTIVITY_LOADED) {
|
|
1394
|
-
loadActivity(true);
|
|
1395
|
-
}
|
|
1396
|
-
}
|
|
1397
|
-
if (next !== "credentials") {
|
|
1398
|
-
$("credentialList").innerHTML = "";
|
|
1399
|
-
setNotice("");
|
|
1400
|
-
}
|
|
1401
|
-
// Focus what is actually on screen. While gated the page views are hidden, so
|
|
1402
|
-
// focusing their heading is a silent no-op that drops focus to <body> and
|
|
1403
|
-
// restarts the next Tab from the top of the document — the gate's own h1 is
|
|
1404
|
-
// the visible heading, and activatePage has just relabelled it.
|
|
1405
|
-
if (options?.focus) $(DATA ? next + "Heading" : "gateHeading").focus();
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
function navigateTo(page, href) {
|
|
1409
|
-
history.pushState({ operatorPage: page }, "", href);
|
|
1410
|
-
activatePage(page, { focus: true });
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
async function load() {
|
|
1414
|
-
const generation = SESSION_GENERATION;
|
|
1415
|
-
let token;
|
|
1416
|
-
try {
|
|
1417
|
-
token = await sessionToken();
|
|
1418
|
-
} catch (e) {
|
|
1419
|
-
if (generation !== SESSION_GENERATION) return;
|
|
1420
|
-
return showGate("Could not read the Clerk session: " + e.message);
|
|
1421
|
-
}
|
|
1422
|
-
if (generation !== SESSION_GENERATION) return;
|
|
1423
|
-
if (!token) return showGate("");
|
|
1424
|
-
let res;
|
|
1425
|
-
try {
|
|
1426
|
-
res = await fetch("/ui/data", { headers: { Authorization: "Bearer " + token } });
|
|
1427
|
-
} catch (e) {
|
|
1428
|
-
if (generation !== SESSION_GENERATION) return;
|
|
1429
|
-
return showGate("Network error: " + e.message);
|
|
1430
|
-
}
|
|
1431
|
-
if (generation !== SESSION_GENERATION) return;
|
|
1432
|
-
if (res.status === 401 || res.status === 403) {
|
|
1433
|
-
if (AUTH.kind === "clerk") {
|
|
1434
|
-
return showGate(
|
|
1435
|
-
res.status === 403
|
|
1436
|
-
? "This Clerk account is not allowed to access connecta."
|
|
1437
|
-
: "Your Clerk session was not accepted. Sign out and try again."
|
|
1438
|
-
);
|
|
1439
|
-
}
|
|
1440
|
-
localStorage.removeItem(KEY);
|
|
1441
|
-
return showGate("Token rejected — enter a valid bearer token.");
|
|
1442
|
-
}
|
|
1443
|
-
if (!res.ok) return showGate("Error " + res.status);
|
|
1444
|
-
let data;
|
|
1445
|
-
try {
|
|
1446
|
-
data = await res.json();
|
|
1447
|
-
} catch (e) {
|
|
1448
|
-
if (generation !== SESSION_GENERATION) return;
|
|
1449
|
-
return showGate("Operator data could not be read.");
|
|
1450
|
-
}
|
|
1451
|
-
if (generation !== SESSION_GENERATION) return;
|
|
1452
|
-
DATA = data;
|
|
1453
|
-
$("gate").classList.add("hidden");
|
|
1454
|
-
$("app").classList.remove("hidden");
|
|
1455
|
-
$("appNav").classList.remove("hidden");
|
|
1456
|
-
const si = DATA.serverInfo || {};
|
|
1457
|
-
$("serverInfo").textContent = (si.name || ${escapeScriptString(brand.productName)}) + " v" + (DATA.connectaVersion || "?");
|
|
1458
|
-
updateCapabilities();
|
|
1459
|
-
activatePage(CURRENT_PAGE);
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
|
-
function actorLabel(actor) {
|
|
1463
|
-
if (!actor || !actor.kind) return "unknown";
|
|
1464
|
-
return actor.label
|
|
1465
|
-
? actor.kind + " · " + actor.label
|
|
1466
|
-
: actor.id ? actor.kind + " · " + actor.id : actor.kind;
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
function renderActivity() {
|
|
1470
|
-
const list = $("activityList");
|
|
1471
|
-
list.innerHTML = "";
|
|
1472
|
-
const query = $("activitySearch").value.trim().toLowerCase();
|
|
1473
|
-
const visible = ACTIVITY.filter((event) => {
|
|
1474
|
-
if (!query) return true;
|
|
1475
|
-
const actor = event.actor || {};
|
|
1476
|
-
return [
|
|
1477
|
-
event.address,
|
|
1478
|
-
event.connectorId,
|
|
1479
|
-
event.toolName,
|
|
1480
|
-
event.source,
|
|
1481
|
-
event.outcome,
|
|
1482
|
-
event.errorCode,
|
|
1483
|
-
actor.kind,
|
|
1484
|
-
actor.id,
|
|
1485
|
-
actor.namespace,
|
|
1486
|
-
actor.label,
|
|
1487
|
-
].some((value) => String(value || "").toLowerCase().includes(query));
|
|
1488
|
-
});
|
|
1489
|
-
const uniqueTools = new Set(ACTIVITY.map((event) => event.address)).size;
|
|
1490
|
-
$("activitySummary").textContent = ACTIVITY.length
|
|
1491
|
-
? ACTIVITY.length + " loaded call" + (ACTIVITY.length === 1 ? "" : "s") +
|
|
1492
|
-
" · " + uniqueTools + " tool" + (uniqueTools === 1 ? "" : "s") +
|
|
1493
|
-
" · no arguments or results stored"
|
|
1494
|
-
: "Arguments and results are never stored.";
|
|
1495
|
-
if (visible.length === 0) {
|
|
1496
|
-
list.innerHTML = '<div class="activity-empty">' +
|
|
1497
|
-
(query ? "No loaded activity matches this search." :
|
|
1498
|
-
"No connector tool calls recorded yet.") + "</div>";
|
|
1499
|
-
}
|
|
1500
|
-
for (const event of visible) {
|
|
1501
|
-
const item = document.createElement("article");
|
|
1502
|
-
const outcomeClass = [
|
|
1503
|
-
"success",
|
|
1504
|
-
"error",
|
|
1505
|
-
"timeout",
|
|
1506
|
-
"cancelled",
|
|
1507
|
-
].includes(event.outcome) ? event.outcome : "error";
|
|
1508
|
-
item.className = "activity-item " + outcomeClass;
|
|
1509
|
-
const retryCopy = event.attempts > 1
|
|
1510
|
-
? " · " + esc(event.attempts) + " attempts"
|
|
1511
|
-
: "";
|
|
1512
|
-
const errorCopy = event.errorCode ? " · " + esc(event.errorCode) : "";
|
|
1513
|
-
const actorId = event.actor?.id
|
|
1514
|
-
? (event.actor.namespace
|
|
1515
|
-
? event.actor.namespace + " · " + event.actor.id
|
|
1516
|
-
: event.actor.id)
|
|
1517
|
-
: "";
|
|
1518
|
-
const stableActorId = actorId &&
|
|
1519
|
-
(event.actor?.label || event.actor?.namespace)
|
|
1520
|
-
? '<div class="activity-actor-id mono">' + esc(actorId) + "</div>"
|
|
1521
|
-
: "";
|
|
1522
|
-
item.innerHTML =
|
|
1523
|
-
'<div class="activity-stamp"><span class="dot ' +
|
|
1524
|
-
(outcomeClass === "success" ? "ok" : "") +
|
|
1525
|
-
'" aria-hidden="true"></span><div><time class="activity-time" datetime="' +
|
|
1526
|
-
esc(event.occurredAt) + '">' +
|
|
1527
|
-
esc(formatDate(event.occurredAt)) +
|
|
1528
|
-
'</time><div class="activity-actor">' + esc(actorLabel(event.actor)) +
|
|
1529
|
-
"</div>" + stableActorId + "</div></div>" +
|
|
1530
|
-
'<div><div class="activity-address">' + esc(event.address) +
|
|
1531
|
-
'</div><div class="activity-detail">' + esc(event.source) + retryCopy +
|
|
1532
|
-
errorCopy + '</div></div>' +
|
|
1533
|
-
'<div><div class="activity-outcome">' + esc(event.outcome) +
|
|
1534
|
-
'</div><div class="activity-detail">' + esc(event.durationMs) + ' ms</div></div>';
|
|
1535
|
-
list.appendChild(item);
|
|
1536
|
-
}
|
|
1537
|
-
$("moreActivity").classList.toggle("hidden", !ACTIVITY_CURSOR);
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
|
-
async function loadActivity(reset) {
|
|
1541
|
-
if (!DATA?.activityEnabled) return;
|
|
1542
|
-
const sessionGeneration = SESSION_GENERATION;
|
|
1543
|
-
if (reset) ACTIVITY_GENERATION += 1;
|
|
1544
|
-
const activityGeneration = ACTIVITY_GENERATION;
|
|
1545
|
-
const isCurrent = () =>
|
|
1546
|
-
sessionGeneration === SESSION_GENERATION &&
|
|
1547
|
-
activityGeneration === ACTIVITY_GENERATION;
|
|
1548
|
-
$("activityNotice").textContent = "Loading activity…";
|
|
1549
|
-
$("activityNotice").setAttribute("role", "status");
|
|
1550
|
-
$("activityList").setAttribute("aria-busy", "true");
|
|
1551
|
-
$("refreshActivity").disabled = true;
|
|
1552
|
-
$("moreActivity").disabled = true;
|
|
1553
|
-
try {
|
|
1554
|
-
const token = await sessionToken();
|
|
1555
|
-
if (!isCurrent()) return;
|
|
1556
|
-
if (!token) return showGate("Your session has expired.");
|
|
1557
|
-
const cursor = reset ? null : ACTIVITY_CURSOR;
|
|
1558
|
-
const params = new URLSearchParams({ limit: "50" });
|
|
1559
|
-
if (cursor) params.set("cursor", cursor);
|
|
1560
|
-
const res = await fetch("/ui/activity?" + params, {
|
|
1561
|
-
headers: { Authorization: "Bearer " + token },
|
|
1562
|
-
});
|
|
1563
|
-
if (!isCurrent()) return;
|
|
1564
|
-
let payload = {};
|
|
1565
|
-
try { payload = await res.json(); } catch (e) {}
|
|
1566
|
-
if (!isCurrent()) return;
|
|
1567
|
-
if (res.status === 401) {
|
|
1568
|
-
return showGate("Your session was not accepted. Sign in again.");
|
|
1569
|
-
}
|
|
1570
|
-
if (res.status === 403) {
|
|
1571
|
-
clearActivityState();
|
|
1572
|
-
$("activityNotice").setAttribute("role", "alert");
|
|
1573
|
-
$("activityNotice").textContent =
|
|
1574
|
-
"This identity may not read activity history.";
|
|
1575
|
-
return;
|
|
1576
|
-
}
|
|
1577
|
-
if (!res.ok) {
|
|
1578
|
-
throw new Error(
|
|
1579
|
-
payload.error || "Activity could not be loaded (" + res.status + ")."
|
|
1580
|
-
);
|
|
1581
|
-
}
|
|
1582
|
-
ACTIVITY = reset
|
|
1583
|
-
? (payload.events || [])
|
|
1584
|
-
: ACTIVITY.concat(payload.events || []);
|
|
1585
|
-
ACTIVITY_CURSOR = payload.nextCursor || null;
|
|
1586
|
-
ACTIVITY_LOADED = true;
|
|
1587
|
-
$("activityNotice").textContent = "";
|
|
1588
|
-
renderActivity();
|
|
1589
|
-
} catch (e) {
|
|
1590
|
-
if (!isCurrent()) return;
|
|
1591
|
-
$("activityNotice").setAttribute("role", "alert");
|
|
1592
|
-
$("activityNotice").textContent = e.message || "Activity could not be loaded.";
|
|
1593
|
-
} finally {
|
|
1594
|
-
if (isCurrent()) {
|
|
1595
|
-
$("activityList").setAttribute("aria-busy", "false");
|
|
1596
|
-
$("refreshActivity").disabled = false;
|
|
1597
|
-
$("moreActivity").disabled = false;
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
function toolkitMcpUrl(name) {
|
|
1603
|
-
const url = new URL(MCP_URL, window.location.href);
|
|
1604
|
-
url.searchParams.set("toolkit", name);
|
|
1605
|
-
return url.toString();
|
|
1606
|
-
}
|
|
1607
|
-
|
|
1608
|
-
function renderToolkits() {
|
|
1609
|
-
const list = $("toolkitList");
|
|
1610
|
-
const toolkits = DATA.toolkits || [];
|
|
1611
|
-
list.innerHTML = "";
|
|
1612
|
-
if (!toolkits.length) {
|
|
1613
|
-
list.innerHTML =
|
|
1614
|
-
'<p class="empty">No toolkits are configured. Unscoped clients see the full connector registry.</p>';
|
|
1615
|
-
return;
|
|
1616
|
-
}
|
|
1617
|
-
const connectors = new Map(
|
|
1618
|
-
DATA.connectors.map((connector) => [connector.id, connector]),
|
|
1619
|
-
);
|
|
1620
|
-
for (const toolkit of toolkits) {
|
|
1621
|
-
const el = document.createElement("article");
|
|
1622
|
-
el.className = "toolkit-card";
|
|
1623
|
-
const connectorCount = toolkit.connectors.length;
|
|
1624
|
-
const toolCount = toolkit.toolCount || 0;
|
|
1625
|
-
let body =
|
|
1626
|
-
'<div class="toolkit-head"><div><div class="toolkit-name">' +
|
|
1627
|
-
'<h3 class="mono">' + esc(toolkit.name) + "</h3>" +
|
|
1628
|
-
'<span class="cap">configured view</span></div>';
|
|
1629
|
-
body += '</div><div class="toolkit-state cap">' +
|
|
1630
|
-
connectorCount + (connectorCount === 1 ? " connector" : " connectors") +
|
|
1631
|
-
" · " + toolCount + (toolCount === 1 ? " loaded tool" : " loaded tools") +
|
|
1632
|
-
"</div></div>";
|
|
1633
|
-
body += '<ul class="toolkit-connectors" aria-label="Included connectors">';
|
|
1634
|
-
for (const id of toolkit.connectors) {
|
|
1635
|
-
const connector = connectors.get(id);
|
|
1636
|
-
body += '<li class="toolkit-connector"><span>' +
|
|
1637
|
-
esc(connector?.title || id) + '</span><code class="mono">' +
|
|
1638
|
-
esc(id) + "</code></li>";
|
|
1639
|
-
}
|
|
1640
|
-
body += "</ul>";
|
|
1641
|
-
body += '<div class="toolkit-rules meta">';
|
|
1642
|
-
if (toolkit.includeTools.length) {
|
|
1643
|
-
body += '<p>Per-connector allowlist: <span class="mono">' +
|
|
1644
|
-
toolkit.includeTools.map(esc).join('</span>, <span class="mono">') +
|
|
1645
|
-
"</span>. Connectors without an allowlist keep all tools.</p>";
|
|
1646
|
-
}
|
|
1647
|
-
if (toolkit.excludeTools.length) {
|
|
1648
|
-
body += '<p>Hidden: <span class="mono">' +
|
|
1649
|
-
toolkit.excludeTools.map(esc).join('</span>, <span class="mono">') +
|
|
1650
|
-
"</span></p>";
|
|
1651
|
-
}
|
|
1652
|
-
if (!toolkit.includeTools.length && !toolkit.excludeTools.length) {
|
|
1653
|
-
body += "<p>All tools on the included connectors are visible.</p>";
|
|
1654
|
-
}
|
|
1655
|
-
body += "</div>";
|
|
1656
|
-
const endpoint = toolkitMcpUrl(toolkit.name);
|
|
1657
|
-
body += '<div class="endpoint toolkit-endpoint"><div class="endpoint-row">' +
|
|
1658
|
-
'<code class="mono">' + esc(endpoint) + "</code>" +
|
|
1659
|
-
'<button class="linklike" type="button" data-toolkit-copy="' +
|
|
1660
|
-
esc(toolkit.name) + '" aria-label="Copy endpoint for ' +
|
|
1661
|
-
esc(toolkit.name) + '">Copy URL</button></div></div>';
|
|
1662
|
-
el.innerHTML = body;
|
|
1663
|
-
list.appendChild(el);
|
|
1664
|
-
}
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
function renderConnections() {
|
|
1668
|
-
const q = $("filter").value.trim().toLowerCase();
|
|
1669
|
-
const list = $("list");
|
|
1670
|
-
list.innerHTML = "";
|
|
1671
|
-
for (const filtered of filterUiConnectors(DATA.connectors, q)) {
|
|
1672
|
-
const c = filtered.connector;
|
|
1673
|
-
const tools = filtered.tools;
|
|
1674
|
-
const el = document.createElement("div");
|
|
1675
|
-
el.className = "card";
|
|
1676
|
-
const status = c.status === "ok"
|
|
1677
|
-
? "Connected"
|
|
1678
|
-
: c.status === "auth_required"
|
|
1679
|
-
? "Authorization needed"
|
|
1680
|
-
: "Unavailable";
|
|
1681
|
-
let head = '<div class="connector-head"><div><div class="connector-title">' +
|
|
1682
|
-
'<span class="dot ' + esc(c.status) + '" aria-hidden="true"></span>' +
|
|
1683
|
-
'<h2>' + esc(c.title || c.id) + "</h2></div>";
|
|
1684
|
-
if (c.description) {
|
|
1685
|
-
head += '<p class="connector-description meta">' + esc(c.description) + "</p>";
|
|
1686
|
-
}
|
|
1687
|
-
const memberships = (DATA.toolkits || [])
|
|
1688
|
-
.filter((toolkit) => toolkit.connectors.includes(c.id))
|
|
1689
|
-
.map((toolkit) => toolkit.name);
|
|
1690
|
-
if (memberships.length) {
|
|
1691
|
-
head += '<div class="connector-toolkits meta"><span>Toolkits</span>' +
|
|
1692
|
-
memberships.map((name) =>
|
|
1693
|
-
'<span class="scope-label">' + esc(name) + "</span>"
|
|
1694
|
-
).join("") + "</div>";
|
|
1695
|
-
}
|
|
1696
|
-
head += '</div><div class="connector-state cap">' + esc(status) + " · " +
|
|
1697
|
-
c.toolCount + (c.toolCount === 1 ? " tool" : " tools") +
|
|
1698
|
-
'<br><span class="mono">' + esc(c.id) + "</span></div></div>";
|
|
1699
|
-
if (c.message) {
|
|
1700
|
-
head += '<p class="connector-message msg">' + esc(c.message) + "</p>";
|
|
1701
|
-
}
|
|
1702
|
-
if (c.credentialCheck) {
|
|
1703
|
-
const check = c.credentialCheck;
|
|
1704
|
-
const verdict = check.state === "ok"
|
|
1705
|
-
? "credential verified"
|
|
1706
|
-
: check.state === "auth_required"
|
|
1707
|
-
? "credential needs authorization"
|
|
1708
|
-
: "credential check failed";
|
|
1709
|
-
head += '<p class="connector-check meta">Credential check: ' +
|
|
1710
|
-
esc(verdict) + " · " + esc(formatDate(check.checkedAt)) +
|
|
1711
|
-
(check.message && check.message !== c.message
|
|
1712
|
-
? " — " + esc(check.message)
|
|
1713
|
-
: "") + "</p>";
|
|
1714
|
-
}
|
|
1715
|
-
if (c.authorizationUrl) {
|
|
1716
|
-
const safe = safeHttp(c.authorizationUrl);
|
|
1717
|
-
head += safe
|
|
1718
|
-
? '<p class="connector-auth"><a class="linklike" href="' + esc(safe) +
|
|
1719
|
-
'" target="_blank" rel="noopener">Authorize connector →</a></p>'
|
|
1720
|
-
: '<p class="connector-auth meta">Authorization URL: ' +
|
|
1721
|
-
esc(c.authorizationUrl) + "</p>";
|
|
1722
|
-
}
|
|
1723
|
-
if (c.oauth && DATA.oauthManagement) {
|
|
1724
|
-
const oauthName = c.title || c.id;
|
|
1725
|
-
head += '<div class="credential-actions">';
|
|
1726
|
-
head += '<button type="button" class="linklike danger" ' +
|
|
1727
|
-
'aria-label="Disconnect OAuth for ' + esc(oauthName) + '" ' +
|
|
1728
|
-
'data-oauth-action="disconnect" data-connector="' + esc(c.id) +
|
|
1729
|
-
'">Disconnect OAuth</button>';
|
|
1730
|
-
head += '<button type="button" class="linklike" ' +
|
|
1731
|
-
'aria-label="' + (c.status === "ok" ? "Reconnect OAuth for " :
|
|
1732
|
-
"Restart authorization for ") + esc(oauthName) + '" ' +
|
|
1733
|
-
'data-oauth-action="reconnect" data-connector="' + esc(c.id) +
|
|
1734
|
-
'">' + (c.status === "ok" ? "Reconnect OAuth" : "Restart authorization") +
|
|
1735
|
-
"</button></div>";
|
|
1736
|
-
}
|
|
1737
|
-
if (c.credential) {
|
|
1738
|
-
head += '<p class="connector-auth"><a class="linklike" href="/credentials" ' +
|
|
1739
|
-
'data-operator-page="credentials">Manage credential →</a></p>';
|
|
1740
|
-
}
|
|
1741
|
-
let body = "";
|
|
1742
|
-
if (tools.length) {
|
|
1743
|
-
body = "<details" + (q ? " open" : "") +
|
|
1744
|
-
'><summary class="linklike">Show tools (' + tools.length + ")</summary>" +
|
|
1745
|
-
'<div class="tool-list">';
|
|
1746
|
-
for (const t of tools) {
|
|
1747
|
-
body += '<div class="tool"><code>' + esc(t.address) + "</code>";
|
|
1748
|
-
if (t.description) body += '<span class="td">' + esc(t.description) + "</span>";
|
|
1749
|
-
body += "</div>";
|
|
1750
|
-
}
|
|
1751
|
-
body += "</div></details>";
|
|
1752
|
-
}
|
|
1753
|
-
el.innerHTML = head + body;
|
|
1754
|
-
list.appendChild(el);
|
|
1755
|
-
}
|
|
1756
|
-
if (!list.children.length) {
|
|
1757
|
-
list.innerHTML = '<p class="empty">' +
|
|
1758
|
-
(q
|
|
1759
|
-
? "No connectors or tools match this filter."
|
|
1760
|
-
: "No connectors are declared in this deployment.") +
|
|
1761
|
-
"</p>";
|
|
1762
|
-
}
|
|
1763
|
-
renderToolkits();
|
|
1764
|
-
}
|
|
1765
|
-
|
|
1766
|
-
async function oauthRequest(connector, method, generation) {
|
|
1767
|
-
const token = await sessionToken();
|
|
1768
|
-
if (generation !== SESSION_GENERATION) {
|
|
1769
|
-
throw new Error("The operator session changed.");
|
|
1770
|
-
}
|
|
1771
|
-
if (!token) throw new Error("Your Clerk session has expired.");
|
|
1772
|
-
const res = await fetch("/ui/oauth/" + encodeURIComponent(connector), {
|
|
1773
|
-
method,
|
|
1774
|
-
headers: { Authorization: "Bearer " + token },
|
|
1775
|
-
});
|
|
1776
|
-
if (res.status === 204) return null;
|
|
1777
|
-
let payload = {};
|
|
1778
|
-
try { payload = await res.json(); } catch (e) {}
|
|
1779
|
-
if (!res.ok) throw new Error(payload.error || "Request failed (" + res.status + ").");
|
|
1780
|
-
return payload;
|
|
1781
|
-
}
|
|
1782
|
-
|
|
1783
|
-
$("list").onclick = async (event) => {
|
|
1784
|
-
const button = event.target.closest("[data-oauth-action]");
|
|
1785
|
-
if (!button) return;
|
|
1786
|
-
const connector = button.dataset.connector;
|
|
1787
|
-
const action = button.dataset.oauthAction;
|
|
1788
|
-
const generation = SESSION_GENERATION;
|
|
1789
|
-
const question = action === "disconnect"
|
|
1790
|
-
? "Disconnect OAuth for " + connector +
|
|
1791
|
-
"? Stored credentials and any pending authorization will be removed."
|
|
1792
|
-
: "Restart OAuth for " + connector +
|
|
1793
|
-
"? Stored credentials and any pending authorization will be replaced.";
|
|
1794
|
-
if (!window.confirm(question)) return;
|
|
1795
|
-
|
|
1796
|
-
setOauthNotice("");
|
|
1797
|
-
const buttons = [...document.querySelectorAll(
|
|
1798
|
-
'[data-connector="' + CSS.escape(connector) + '"]'
|
|
1799
|
-
)];
|
|
1800
|
-
buttons.forEach((item) => { item.disabled = true; });
|
|
1801
|
-
try {
|
|
1802
|
-
const result = await oauthRequest(
|
|
1803
|
-
connector,
|
|
1804
|
-
action === "disconnect" ? "DELETE" : "POST",
|
|
1805
|
-
generation,
|
|
1806
|
-
);
|
|
1807
|
-
if (generation !== SESSION_GENERATION) return;
|
|
1808
|
-
await load();
|
|
1809
|
-
if (generation !== SESSION_GENERATION) return;
|
|
1810
|
-
setOauthNotice(
|
|
1811
|
-
action === "disconnect"
|
|
1812
|
-
? "OAuth disconnected. Restart authorization when you are ready to reconnect."
|
|
1813
|
-
: result?.message ||
|
|
1814
|
-
"Authorization restarted. Open the authorization link to reconnect.",
|
|
1815
|
-
);
|
|
1816
|
-
$("oauthNotice").focus();
|
|
1817
|
-
} catch (e) {
|
|
1818
|
-
if (generation !== SESSION_GENERATION) return;
|
|
1819
|
-
// A reset can publish its durable epoch before best-effort physical cleanup
|
|
1820
|
-
// reports failure. Refresh so stale tools/health/actions never survive that
|
|
1821
|
-
// partial success, while preserving the original error for the operator.
|
|
1822
|
-
try {
|
|
1823
|
-
await load();
|
|
1824
|
-
} catch (refreshError) {
|
|
1825
|
-
// The mutation error is the actionable one; load() already owns its gate.
|
|
1826
|
-
}
|
|
1827
|
-
if (generation !== SESSION_GENERATION) return;
|
|
1828
|
-
setOauthNotice(e.message || "OAuth action failed.", true);
|
|
1829
|
-
$("oauthNotice").focus();
|
|
1830
|
-
} finally {
|
|
1831
|
-
if (generation === SESSION_GENERATION) {
|
|
1832
|
-
buttons.forEach((item) => { item.disabled = false; });
|
|
1833
|
-
}
|
|
1834
|
-
}
|
|
1835
|
-
};
|
|
1836
|
-
|
|
1837
|
-
function renderCredentials() {
|
|
1838
|
-
const list = $("credentialList");
|
|
1839
|
-
list.innerHTML = "";
|
|
1840
|
-
if (DATA.credentialManagement !== "available") return;
|
|
1841
|
-
for (const c of DATA.connectors.filter((connector) => connector.credential)) {
|
|
1842
|
-
const cred = c.credential;
|
|
1843
|
-
const configured = Boolean(cred.configured);
|
|
1844
|
-
const removable = configured || Boolean(cred.removable);
|
|
1845
|
-
const state = configured
|
|
1846
|
-
? cred.fields?.length
|
|
1847
|
-
? "configured"
|
|
1848
|
-
: "configured · ••••" + esc(cred.lastFour || "")
|
|
1849
|
-
: "not configured";
|
|
1850
|
-
const updated = configured && cred.updatedAt
|
|
1851
|
-
? " · updated " + esc(formatDate(cred.updatedAt))
|
|
1852
|
-
: "";
|
|
1853
|
-
const el = document.createElement("section");
|
|
1854
|
-
el.className = "credential-card";
|
|
1855
|
-
el.id = "credential-" + c.id;
|
|
1856
|
-
el.setAttribute("aria-labelledby", "credential-title-" + c.id);
|
|
1857
|
-
let body = '<div class="credential-head"><div class="connector-title">' +
|
|
1858
|
-
'<span class="dot ' + (configured ? "ok" : "auth_required") +
|
|
1859
|
-
'" aria-hidden="true"></span><h2 id="credential-title-' + esc(c.id) + '">' +
|
|
1860
|
-
esc(c.title || c.id) + '</h2></div><span class="credential-state">' +
|
|
1861
|
-
state + updated + "</span></div>";
|
|
1862
|
-
body += '<p class="mono">' + esc(c.id) + " · " + esc(cred.label) + "</p>";
|
|
1863
|
-
if (cred.description) {
|
|
1864
|
-
body += '<p class="credential-copy meta">' + esc(cred.description) + "</p>";
|
|
1865
|
-
}
|
|
1866
|
-
if (cred.fields?.length) {
|
|
1867
|
-
body += '<div class="credential-field-summary">';
|
|
1868
|
-
for (const field of cred.fields) {
|
|
1869
|
-
const fieldState = field.configured
|
|
1870
|
-
? "configured · ••••" + esc(field.lastFour || "") +
|
|
1871
|
-
(field.updatedAt ? " · updated " + esc(formatDate(field.updatedAt)) : "")
|
|
1872
|
-
: "not configured";
|
|
1873
|
-
body += '<div><span>' + esc(field.label) +
|
|
1874
|
-
'</span><span class="meta">' + fieldState + "</span></div>";
|
|
1875
|
-
}
|
|
1876
|
-
body += "</div>";
|
|
1877
|
-
}
|
|
1878
|
-
if (c.credentialCheck) {
|
|
1879
|
-
const check = c.credentialCheck;
|
|
1880
|
-
const verdict = check.state === "ok"
|
|
1881
|
-
? "healthy"
|
|
1882
|
-
: check.state === "auth_required"
|
|
1883
|
-
? "needs authorization"
|
|
1884
|
-
: "check failed";
|
|
1885
|
-
body += '<p class="connector-check meta">Liveness: ' + esc(verdict) +
|
|
1886
|
-
" · " + esc(formatDate(check.checkedAt)) +
|
|
1887
|
-
(check.message ? " — " + esc(check.message) : "") + "</p>";
|
|
1888
|
-
}
|
|
1889
|
-
if (cred.error) body += '<div class="msg">' + esc(cred.error) + "</div>";
|
|
1890
|
-
// Leftover stored fields are not an error — the credential still works, so
|
|
1891
|
-
// this stays muted copy rather than the msg block an actual failure earns.
|
|
1892
|
-
if (cred.notice) {
|
|
1893
|
-
body += '<p class="credential-copy meta">' + esc(cred.notice) + "</p>";
|
|
1894
|
-
}
|
|
1895
|
-
body += '<div class="credential-actions">';
|
|
1896
|
-
body += '<button class="linklike" type="button" data-credential-action="edit" data-connector="' +
|
|
1897
|
-
esc(c.id) + '">' + (removable ? "Replace" : "Add credential") + "</button>";
|
|
1898
|
-
if (configured && cred.testable) {
|
|
1899
|
-
body += '<button class="linklike" type="button" data-credential-action="test" data-connector="' +
|
|
1900
|
-
esc(c.id) + '">Test</button>';
|
|
1901
|
-
}
|
|
1902
|
-
if (removable) {
|
|
1903
|
-
body += '<button type="button" class="linklike danger" data-credential-action="remove" data-connector="' +
|
|
1904
|
-
esc(c.id) + '">Remove</button>';
|
|
1905
|
-
}
|
|
1906
|
-
body += "</div>";
|
|
1907
|
-
body += '<div class="credential-form hidden" data-credential-form="' +
|
|
1908
|
-
esc(c.id) + '">';
|
|
1909
|
-
if (cred.fields && cred.fields.length) {
|
|
1910
|
-
body += '<div class="credential-fields">';
|
|
1911
|
-
for (let index = 0; index < cred.fields.length; index += 1) {
|
|
1912
|
-
const field = cred.fields[index];
|
|
1913
|
-
const inputId = "credential-input-" + c.id + "-" + index;
|
|
1914
|
-
body += '<div class="credential-field"><label for="' + esc(inputId) + '">' +
|
|
1915
|
-
esc(field.label) + '</label><input id="' + esc(inputId) + '" type="' +
|
|
1916
|
-
esc(field.inputType || "password") + '" data-credential-field="' +
|
|
1917
|
-
esc(field.name) + '" placeholder="' + esc(field.placeholder || field.label) +
|
|
1918
|
-
'" autocomplete="' +
|
|
1919
|
-
(field.inputType === "password" ? "new-password" : "off") +
|
|
1920
|
-
'" autocapitalize="none" spellcheck="false"></div>';
|
|
1921
|
-
}
|
|
1922
|
-
body += "</div>";
|
|
1923
|
-
} else {
|
|
1924
|
-
const inputId = "credential-input-" + c.id;
|
|
1925
|
-
body += '<label class="visually-hidden" for="' + esc(inputId) + '">' +
|
|
1926
|
-
esc(cred.label) + '</label><input id="' + esc(inputId) +
|
|
1927
|
-
'" type="password" data-credential-input="' +
|
|
1928
|
-
esc(c.id) + '" aria-label="' + esc(cred.label) + '" placeholder="' +
|
|
1929
|
-
esc(cred.placeholder || "Paste credential") +
|
|
1930
|
-
'" autocomplete="new-password" autocapitalize="none" spellcheck="false">';
|
|
1931
|
-
}
|
|
1932
|
-
body += '<button class="linklike" type="button" data-credential-action="save" data-connector="' +
|
|
1933
|
-
esc(c.id) + '">Save</button><button class="linklike" type="button" data-credential-action="cancel" data-connector="' +
|
|
1934
|
-
esc(c.id) + '">Cancel</button></div>';
|
|
1935
|
-
el.innerHTML = body;
|
|
1936
|
-
list.appendChild(el);
|
|
1937
|
-
}
|
|
1938
|
-
}
|
|
1939
|
-
|
|
1940
|
-
async function credentialRequest(connector, method, action, body, generation) {
|
|
1941
|
-
const token = await sessionToken();
|
|
1942
|
-
if (generation !== SESSION_GENERATION) {
|
|
1943
|
-
throw new Error("The operator session changed.");
|
|
1944
|
-
}
|
|
1945
|
-
if (!token) throw new Error("Your Clerk session has expired.");
|
|
1946
|
-
const suffix = action ? "/" + action : "";
|
|
1947
|
-
const res = await fetch(
|
|
1948
|
-
"/ui/credentials/" + encodeURIComponent(connector) + suffix,
|
|
1949
|
-
{
|
|
1950
|
-
method,
|
|
1951
|
-
headers: {
|
|
1952
|
-
Authorization: "Bearer " + token,
|
|
1953
|
-
...(body ? { "Content-Type": "application/json" } : {}),
|
|
1954
|
-
},
|
|
1955
|
-
...(body ? { body: JSON.stringify(body) } : {}),
|
|
1956
|
-
},
|
|
1957
|
-
);
|
|
1958
|
-
if (res.status === 204) return null;
|
|
1959
|
-
let payload = {};
|
|
1960
|
-
try { payload = await res.json(); } catch (e) {}
|
|
1961
|
-
if (!res.ok) throw new Error(payload.error || "Request failed (" + res.status + ").");
|
|
1962
|
-
return payload;
|
|
1963
|
-
}
|
|
1964
|
-
|
|
1965
|
-
function credentialForm(connector) {
|
|
1966
|
-
return document.querySelector('[data-credential-form="' +
|
|
1967
|
-
CSS.escape(connector) + '"]');
|
|
1968
|
-
}
|
|
1969
|
-
|
|
1970
|
-
$("credentialList").onclick = async (event) => {
|
|
1971
|
-
const button = event.target.closest("[data-credential-action]");
|
|
1972
|
-
if (!button) return;
|
|
1973
|
-
const connector = button.dataset.connector;
|
|
1974
|
-
const action = button.dataset.credentialAction;
|
|
1975
|
-
const form = credentialForm(connector);
|
|
1976
|
-
const generation = SESSION_GENERATION;
|
|
1977
|
-
|
|
1978
|
-
if (action === "edit") {
|
|
1979
|
-
form.classList.remove("hidden");
|
|
1980
|
-
form.querySelector("input")?.focus();
|
|
1981
|
-
return;
|
|
1982
|
-
}
|
|
1983
|
-
if (action === "cancel") {
|
|
1984
|
-
form.querySelectorAll("input").forEach((input) => { input.value = ""; });
|
|
1985
|
-
form.classList.add("hidden");
|
|
1986
|
-
document.querySelector(
|
|
1987
|
-
'[data-credential-action="edit"][data-connector="' +
|
|
1988
|
-
CSS.escape(connector) + '"]'
|
|
1989
|
-
)?.focus();
|
|
1990
|
-
return;
|
|
1991
|
-
}
|
|
1992
|
-
if (action === "remove" && !window.confirm(
|
|
1993
|
-
"Remove this credential? The connector will stop authenticating until a replacement is added."
|
|
1994
|
-
)) return;
|
|
1995
|
-
|
|
1996
|
-
setNotice("");
|
|
1997
|
-
$("credentialList").setAttribute("aria-busy", "true");
|
|
1998
|
-
const buttons = [...document.querySelectorAll(
|
|
1999
|
-
'[data-connector="' + CSS.escape(connector) + '"]'
|
|
2000
|
-
)];
|
|
2001
|
-
buttons.forEach((item) => { item.disabled = true; });
|
|
2002
|
-
try {
|
|
2003
|
-
if (action === "save") {
|
|
2004
|
-
const fieldInputs = [...form.querySelectorAll("[data-credential-field]")];
|
|
2005
|
-
if (fieldInputs.length) {
|
|
2006
|
-
const values = {};
|
|
2007
|
-
for (const input of fieldInputs) {
|
|
2008
|
-
const value = input.value.trim();
|
|
2009
|
-
if (!value) throw new Error("Complete every credential field before saving.");
|
|
2010
|
-
values[input.dataset.credentialField] = value;
|
|
2011
|
-
}
|
|
2012
|
-
await credentialRequest(connector, "PUT", "", { values }, generation);
|
|
2013
|
-
} else {
|
|
2014
|
-
const input = form.querySelector("[data-credential-input]");
|
|
2015
|
-
const value = input.value.trim();
|
|
2016
|
-
if (!value) throw new Error("Paste a credential before saving.");
|
|
2017
|
-
await credentialRequest(connector, "PUT", "", { value }, generation);
|
|
2018
|
-
}
|
|
2019
|
-
if (generation !== SESSION_GENERATION) return;
|
|
2020
|
-
form.querySelectorAll("input").forEach((input) => { input.value = ""; });
|
|
2021
|
-
setNotice("Credential saved.");
|
|
2022
|
-
await load();
|
|
2023
|
-
if (generation !== SESSION_GENERATION) return;
|
|
2024
|
-
$("credentialNotice").focus();
|
|
2025
|
-
} else if (action === "remove") {
|
|
2026
|
-
await credentialRequest(connector, "DELETE", "", null, generation);
|
|
2027
|
-
if (generation !== SESSION_GENERATION) return;
|
|
2028
|
-
setNotice("Credential removed.");
|
|
2029
|
-
await load();
|
|
2030
|
-
if (generation !== SESSION_GENERATION) return;
|
|
2031
|
-
$("credentialNotice").focus();
|
|
2032
|
-
} else if (action === "test") {
|
|
2033
|
-
const result = await credentialRequest(
|
|
2034
|
-
connector,
|
|
2035
|
-
"POST",
|
|
2036
|
-
"test",
|
|
2037
|
-
null,
|
|
2038
|
-
generation,
|
|
2039
|
-
);
|
|
2040
|
-
if (generation !== SESSION_GENERATION) return;
|
|
2041
|
-
setNotice(
|
|
2042
|
-
result.message || (result.ok ? "Credential is valid." : "Credential test failed."),
|
|
2043
|
-
!result.ok,
|
|
2044
|
-
);
|
|
2045
|
-
}
|
|
2046
|
-
} catch (e) {
|
|
2047
|
-
if (generation !== SESSION_GENERATION) return;
|
|
2048
|
-
setNotice(e.message || "Credential action failed.", true);
|
|
2049
|
-
} finally {
|
|
2050
|
-
if (generation === SESSION_GENERATION) {
|
|
2051
|
-
$("credentialList").setAttribute("aria-busy", "false");
|
|
2052
|
-
buttons.forEach((item) => { item.disabled = false; });
|
|
2053
|
-
}
|
|
2054
|
-
}
|
|
2055
|
-
};
|
|
2056
|
-
|
|
2057
|
-
$("save").onclick = async () => {
|
|
2058
|
-
const v = $("token").value.trim();
|
|
2059
|
-
if (!v) return;
|
|
2060
|
-
clearIdentityState();
|
|
2061
|
-
localStorage.setItem(KEY, v);
|
|
2062
|
-
$("token").value = "";
|
|
2063
|
-
await load();
|
|
2064
|
-
if (DATA) $(CURRENT_PAGE + "Heading").focus();
|
|
2065
|
-
};
|
|
2066
|
-
$("change").onclick = () => {
|
|
2067
|
-
localStorage.removeItem(KEY);
|
|
2068
|
-
showGate("");
|
|
2069
|
-
$("token").focus();
|
|
2070
|
-
};
|
|
2071
|
-
$("copyMcpUrl").onclick = async () => {
|
|
2072
|
-
const button = $("copyMcpUrl");
|
|
2073
|
-
try {
|
|
2074
|
-
await navigator.clipboard.writeText(MCP_URL);
|
|
2075
|
-
button.textContent = "Copied";
|
|
2076
|
-
} catch (e) {
|
|
2077
|
-
button.textContent = "Copy failed";
|
|
2078
|
-
}
|
|
2079
|
-
window.setTimeout(() => { button.textContent = "Copy URL"; }, 1600);
|
|
2080
|
-
};
|
|
2081
|
-
$("toolkitList").onclick = async (event) => {
|
|
2082
|
-
const button = event.target.closest("[data-toolkit-copy]");
|
|
2083
|
-
if (!button) return;
|
|
2084
|
-
try {
|
|
2085
|
-
await navigator.clipboard.writeText(
|
|
2086
|
-
toolkitMcpUrl(button.dataset.toolkitCopy),
|
|
2087
|
-
);
|
|
2088
|
-
button.textContent = "Copied";
|
|
2089
|
-
} catch (e) {
|
|
2090
|
-
button.textContent = "Copy failed";
|
|
2091
|
-
}
|
|
2092
|
-
window.setTimeout(() => { button.textContent = "Copy URL"; }, 1600);
|
|
2093
|
-
};
|
|
2094
|
-
$("signin").onclick = () => Clerk.redirectToSignIn({
|
|
2095
|
-
signInFallbackRedirectUrl: window.location.href,
|
|
2096
|
-
signUpFallbackRedirectUrl: window.location.href,
|
|
2097
|
-
});
|
|
2098
|
-
function signOut() {
|
|
2099
|
-
clearIdentityState();
|
|
2100
|
-
return Clerk.signOut({ redirectUrl: window.location.href });
|
|
2101
|
-
}
|
|
2102
|
-
$("gateSignout").onclick = signOut;
|
|
2103
|
-
$("signout").onclick = signOut;
|
|
2104
|
-
$("filter").oninput = () => { if (DATA) renderConnections(); };
|
|
2105
|
-
$("refreshActivity").onclick = () => loadActivity(true);
|
|
2106
|
-
$("moreActivity").onclick = () => loadActivity(false);
|
|
2107
|
-
$("activitySearch").oninput = () => renderActivity();
|
|
2108
|
-
document.addEventListener("click", (event) => {
|
|
2109
|
-
const link = event.target.closest("a[data-operator-page]");
|
|
2110
|
-
if (
|
|
2111
|
-
!link ||
|
|
2112
|
-
event.defaultPrevented ||
|
|
2113
|
-
event.button !== 0 ||
|
|
2114
|
-
event.metaKey ||
|
|
2115
|
-
event.ctrlKey ||
|
|
2116
|
-
event.shiftKey ||
|
|
2117
|
-
event.altKey
|
|
2118
|
-
) return;
|
|
2119
|
-
const target = new URL(link.href, window.location.href);
|
|
2120
|
-
if (target.origin !== window.location.origin) return;
|
|
2121
|
-
event.preventDefault();
|
|
2122
|
-
navigateTo(link.dataset.operatorPage, target.pathname + target.search + target.hash);
|
|
2123
|
-
});
|
|
2124
|
-
window.addEventListener("popstate", () => {
|
|
2125
|
-
activatePage(pageForPath(window.location.pathname), { focus: true });
|
|
2126
|
-
});
|
|
2127
|
-
|
|
2128
|
-
async function init() {
|
|
2129
|
-
if (AUTH.kind === "clerk") {
|
|
2130
|
-
$("clerkGate").classList.remove("hidden");
|
|
2131
|
-
$("signout").classList.remove("hidden");
|
|
2132
|
-
try {
|
|
2133
|
-
if (!window.Clerk) {
|
|
2134
|
-
return showGate("Clerk could not load. Check your network and try again.");
|
|
2135
|
-
}
|
|
2136
|
-
await Clerk.load({
|
|
2137
|
-
...(AUTH.signInUrl ? { signInUrl: AUTH.signInUrl } : {}),
|
|
2138
|
-
...(AUTH.signUpUrl ? { signUpUrl: AUTH.signUpUrl } : {}),
|
|
2139
|
-
signInFallbackRedirectUrl: window.location.href,
|
|
2140
|
-
signUpFallbackRedirectUrl: window.location.href,
|
|
2141
|
-
afterSignOutUrl: window.location.href,
|
|
2142
|
-
});
|
|
2143
|
-
CLERK_SESSION_ID = Clerk.session?.id ?? null;
|
|
2144
|
-
Clerk.addListener((resources) => {
|
|
2145
|
-
const nextSessionId = resources.session?.id ?? null;
|
|
2146
|
-
if (nextSessionId === CLERK_SESSION_ID) return;
|
|
2147
|
-
CLERK_SESSION_ID = nextSessionId;
|
|
2148
|
-
// Clerk has already updated its public session before notifying
|
|
2149
|
-
// listeners. Clear synchronously so stale identity-scoped data cannot
|
|
2150
|
-
// be repainted while the replacement identity is being fetched.
|
|
2151
|
-
showGate("");
|
|
2152
|
-
void load();
|
|
2153
|
-
});
|
|
2154
|
-
} catch (e) {
|
|
2155
|
-
return showGate("Clerk could not initialize: " + e.message);
|
|
2156
|
-
}
|
|
2157
|
-
} else {
|
|
2158
|
-
$("tokenGate").classList.remove("hidden");
|
|
2159
|
-
$("change").classList.remove("hidden");
|
|
2160
|
-
}
|
|
2161
|
-
activatePage(pageForPath(window.location.pathname));
|
|
2162
|
-
await load();
|
|
2163
|
-
}
|
|
2164
|
-
|
|
2165
|
-
if (AUTH.kind === "clerk") {
|
|
2166
|
-
window.addEventListener("load", init);
|
|
2167
|
-
} else {
|
|
2168
|
-
init();
|
|
2169
|
-
}
|
|
2170
|
-
</script>
|
|
690
|
+
const PRODUCT_NAME = ${stringForInlineScript(brand.productName)};
|
|
691
|
+
const PRODUCT_OPERATOR_LABEL = ${stringForInlineScript(brand.productName + " operator")};
|
|
692
|
+
${OPERATOR_UI_SCRIPT}</script>
|
|
2171
693
|
</body>
|
|
2172
694
|
</html>`;
|
|
2173
695
|
}
|