@zackbart/connecta 0.7.1 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/README.md +5 -2
  3. package/dist/connector-scope.d.ts +7 -4
  4. package/dist/connector-scope.d.ts.map +1 -1
  5. package/dist/connector-scope.js +36 -16
  6. package/dist/connector-scope.js.map +1 -1
  7. package/dist/connectors/remote-mcp.d.ts.map +1 -1
  8. package/dist/connectors/remote-mcp.js +164 -95
  9. package/dist/connectors/remote-mcp.js.map +1 -1
  10. package/dist/credential-health.d.ts +11 -7
  11. package/dist/credential-health.d.ts.map +1 -1
  12. package/dist/credential-health.js +26 -19
  13. package/dist/credential-health.js.map +1 -1
  14. package/dist/execute.d.ts +3 -0
  15. package/dist/execute.d.ts.map +1 -1
  16. package/dist/execute.js +70 -29
  17. package/dist/execute.js.map +1 -1
  18. package/dist/executor-admission.d.ts +53 -0
  19. package/dist/executor-admission.d.ts.map +1 -0
  20. package/dist/executor-admission.js +151 -0
  21. package/dist/executor-admission.js.map +1 -0
  22. package/dist/executor-result.d.ts +13 -0
  23. package/dist/executor-result.d.ts.map +1 -0
  24. package/dist/executor-result.js +57 -0
  25. package/dist/executor-result.js.map +1 -0
  26. package/dist/executors/quickjs-child.d.ts +2 -0
  27. package/dist/executors/quickjs-child.d.ts.map +1 -0
  28. package/dist/executors/quickjs-child.js +131 -0
  29. package/dist/executors/quickjs-child.js.map +1 -0
  30. package/dist/executors/quickjs-protocol.d.ts +55 -0
  31. package/dist/executors/quickjs-protocol.d.ts.map +1 -0
  32. package/dist/executors/quickjs-protocol.js +22 -0
  33. package/dist/executors/quickjs-protocol.js.map +1 -0
  34. package/dist/executors/quickjs-runtime.d.ts +26 -0
  35. package/dist/executors/quickjs-runtime.d.ts.map +1 -0
  36. package/dist/executors/quickjs-runtime.js +383 -0
  37. package/dist/executors/quickjs-runtime.js.map +1 -0
  38. package/dist/executors/quickjs.d.ts +20 -15
  39. package/dist/executors/quickjs.d.ts.map +1 -1
  40. package/dist/executors/quickjs.js +465 -265
  41. package/dist/executors/quickjs.js.map +1 -1
  42. package/dist/index.d.ts +15 -3
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +7 -0
  45. package/dist/index.js.map +1 -1
  46. package/dist/meta-tools.d.ts +4 -0
  47. package/dist/meta-tools.d.ts.map +1 -1
  48. package/dist/meta-tools.js +49 -12
  49. package/dist/meta-tools.js.map +1 -1
  50. package/dist/node.d.ts.map +1 -1
  51. package/dist/node.js +36 -3
  52. package/dist/node.js.map +1 -1
  53. package/dist/registry.d.ts +21 -24
  54. package/dist/registry.d.ts.map +1 -1
  55. package/dist/registry.js +37 -25
  56. package/dist/registry.js.map +1 -1
  57. package/dist/server.d.ts.map +1 -1
  58. package/dist/server.js +19 -9
  59. package/dist/server.js.map +1 -1
  60. package/dist/timeout.d.ts +9 -4
  61. package/dist/timeout.d.ts.map +1 -1
  62. package/dist/timeout.js +34 -4
  63. package/dist/timeout.js.map +1 -1
  64. package/dist/toolkits.d.ts +8 -0
  65. package/dist/toolkits.d.ts.map +1 -1
  66. package/dist/toolkits.js +3 -0
  67. package/dist/toolkits.js.map +1 -1
  68. package/dist/types.d.ts +19 -2
  69. package/dist/types.d.ts.map +1 -1
  70. package/dist/ui.d.ts +13 -1
  71. package/dist/ui.d.ts.map +1 -1
  72. package/dist/ui.js +188 -7
  73. package/dist/ui.js.map +1 -1
  74. package/dist/version.d.ts +1 -1
  75. package/dist/version.js +1 -1
  76. package/package.json +1 -1
  77. package/src/connector-scope.ts +43 -18
  78. package/src/connectors/remote-mcp.ts +184 -102
  79. package/src/credential-health.ts +33 -17
  80. package/src/execute.ts +93 -35
  81. package/src/executor-admission.ts +229 -0
  82. package/src/executor-result.ts +63 -0
  83. package/src/executors/quickjs-child.ts +168 -0
  84. package/src/executors/quickjs-protocol.ts +77 -0
  85. package/src/executors/quickjs-runtime.ts +440 -0
  86. package/src/executors/quickjs.ts +669 -302
  87. package/src/index.ts +30 -5
  88. package/src/meta-tools.ts +84 -27
  89. package/src/node.ts +36 -2
  90. package/src/registry.ts +53 -22
  91. package/src/server.ts +21 -10
  92. package/src/timeout.ts +41 -4
  93. package/src/toolkits.ts +11 -0
  94. package/src/types.ts +19 -2
  95. package/src/ui.ts +218 -12
  96. package/src/version.ts +1 -1
package/src/server.ts CHANGED
@@ -222,8 +222,8 @@ async function authorize(
222
222
  | {
223
223
  ok: true;
224
224
  actor: ActivityActor;
225
- providerKind?: string;
226
- userId?: string;
225
+ /** True only when the admitting provider can also authorize UI mutation. */
226
+ uiAdminEligible?: boolean;
227
227
  /** The admitting identity's toolkit binding (docs/toolkits.md). */
228
228
  toolkitBinding?: ToolkitBinding;
229
229
  }
@@ -261,8 +261,9 @@ async function authorize(
261
261
  kind: provider.kind,
262
262
  ...(subjectId ? { id: subjectId } : {}),
263
263
  },
264
- providerKind: provider.kind,
265
- ...(result.userId ? { userId: result.userId } : {}),
264
+ ...(result.userId && provider.uiAuth?.kind === "clerk"
265
+ ? { uiAdminEligible: true }
266
+ : {}),
266
267
  ...(binding.binding ? { toolkitBinding: binding.binding } : {}),
267
268
  };
268
269
  }
@@ -896,6 +897,9 @@ async function serveMcp(
896
897
  activity,
897
898
  defaultToolTimeoutMs: opts.defaultToolTimeoutMs,
898
899
  probeTimeoutMs: opts.probeTimeoutMs,
900
+ ...(runtimeContext
901
+ ? { defer: runtimeContext.waitUntil.bind(runtimeContext) }
902
+ : {}),
899
903
  });
900
904
  if (opts.executor) {
901
905
  registerExecuteTool(server, registry, {
@@ -903,6 +907,7 @@ async function serveMcp(
903
907
  executor: opts.executor,
904
908
  logger: opts.logger,
905
909
  activity,
910
+ requestSignal: request.signal,
906
911
  });
907
912
  }
908
913
  const transport = new WebStandardStreamableHTTPServerTransport({
@@ -1044,6 +1049,9 @@ export function createFetchHandler(
1044
1049
  const url = new URL(request.url);
1045
1050
  const baseUrl = publicUrl ?? url.origin;
1046
1051
  const path = url.pathname;
1052
+ const defer = runtimeContext
1053
+ ? runtimeContext.waitUntil.bind(runtimeContext)
1054
+ : undefined;
1047
1055
 
1048
1056
  /**
1049
1057
  * Piggyback a DUE credential liveness sweep on traffic that has already been
@@ -1059,7 +1067,7 @@ export function createFetchHandler(
1059
1067
  // throws while deciding whether anything is due. Nothing about a
1060
1068
  // background health check may turn a served request into a 500.
1061
1069
  try {
1062
- const sweep = registry.sweepCredentialHealthIfDue(baseUrl);
1070
+ const sweep = registry.sweepCredentialHealthIfDue(baseUrl, defer);
1063
1071
  if (!sweep) return;
1064
1072
  const settled = sweep.then(
1065
1073
  () => {},
@@ -1067,7 +1075,7 @@ export function createFetchHandler(
1067
1075
  opts.logger.warn("[connecta] credential health sweep failed", err);
1068
1076
  },
1069
1077
  );
1070
- if (runtimeContext?.waitUntil) runtimeContext.waitUntil(settled);
1078
+ if (defer) defer(settled);
1071
1079
  else void settled;
1072
1080
  } catch (err) {
1073
1081
  opts.logger.warn("[connecta] credential health sweep failed", err);
@@ -1197,7 +1205,7 @@ export function createFetchHandler(
1197
1205
 
1198
1206
  const operatorPage = operatorPageForPath(path);
1199
1207
  if (operatorPage) {
1200
- if (request.method !== "GET") {
1208
+ if (request.method !== "GET" && request.method !== "HEAD") {
1201
1209
  return privateJson({ error: "method not allowed" }, { status: 405 });
1202
1210
  }
1203
1211
  // Open shell — carries no operator data; everything comes from the
@@ -1212,7 +1220,9 @@ export function createFetchHandler(
1212
1220
  // — only script execution, the XSS sink, is gated.
1213
1221
  const nonce = uiScriptNonce();
1214
1222
  return new Response(
1215
- renderUiHtml(uiAuth, mcpUrl, opts.branding, nonce, operatorPage),
1223
+ request.method === "HEAD"
1224
+ ? null
1225
+ : renderUiHtml(uiAuth, mcpUrl, opts.branding, nonce, operatorPage),
1216
1226
  {
1217
1227
  status: 200,
1218
1228
  headers: {
@@ -1235,8 +1245,7 @@ export function createFetchHandler(
1235
1245
  // After the restriction check, not before: an identity that may not
1236
1246
  // read this surface should not get to trigger background work from it.
1237
1247
  sweepCredentials();
1238
- const eligibleClerkOperator =
1239
- authz.providerKind === "clerk" && Boolean(authz.userId);
1248
+ const eligibleClerkOperator = authz.uiAdminEligible === true;
1240
1249
  const credentialManagement = credentialManagementCapability({
1241
1250
  eligibleClerkOperator,
1242
1251
  hasCredentialSlots: registry
@@ -1253,6 +1262,8 @@ export function createFetchHandler(
1253
1262
  eligibleClerkOperator ? opts.credentialVault : undefined,
1254
1263
  Boolean(opts.activity?.list),
1255
1264
  credentialManagement,
1265
+ opts.toolkits,
1266
+ defer,
1256
1267
  );
1257
1268
  return privateJson(data);
1258
1269
  }
package/src/timeout.ts CHANGED
@@ -21,10 +21,9 @@ export function normalizeTimeoutMs(
21
21
 
22
22
  /**
23
23
  * Reject `promise` after `ms` if it has not settled, so one hung downstream
24
- * cannot stall a whole fan-out. NOTE: this bounds only the caller-facing wait
25
- * the registry probe methods take no AbortSignal, so the underlying fetch is
26
- * NOT cancelled and keeps running in the background. Real cancellation
27
- * (AbortSignal plumbed through the registry) is a deferred follow-up.
24
+ * cannot stall a whole fan-out. This form bounds only the caller-facing wait;
25
+ * use `withAbortableTimeout` when the operation accepts an AbortSignal and the
26
+ * underlying work must stop too.
28
27
  */
29
28
  export function withTimeout<T>(
30
29
  promise: Promise<T>,
@@ -47,3 +46,41 @@ export function withTimeout<T>(
47
46
  );
48
47
  });
49
48
  }
49
+
50
+ /**
51
+ * Give one operation a caller-facing deadline and the matching cancellation
52
+ * signal. The timeout rejects with the stable, labelled error while aborting
53
+ * any in-flight work that honors the signal.
54
+ */
55
+ export function withAbortableTimeout<T>(
56
+ operation: (signal: AbortSignal) => Promise<T>,
57
+ ms: number,
58
+ label: string,
59
+ ): Promise<T> {
60
+ const controller = new AbortController();
61
+ return new Promise<T>((resolve, reject) => {
62
+ const timeoutError = new Error(`${label} timed out after ${ms}ms`);
63
+ const timer = setTimeout(() => {
64
+ controller.abort(timeoutError);
65
+ reject(timeoutError);
66
+ }, ms);
67
+ let promise: Promise<T>;
68
+ try {
69
+ promise = operation(controller.signal);
70
+ } catch (err) {
71
+ clearTimeout(timer);
72
+ reject(err);
73
+ return;
74
+ }
75
+ promise.then(
76
+ (value) => {
77
+ clearTimeout(timer);
78
+ resolve(value);
79
+ },
80
+ (err) => {
81
+ clearTimeout(timer);
82
+ reject(err);
83
+ },
84
+ );
85
+ });
86
+ }
package/src/toolkits.ts CHANGED
@@ -41,6 +41,14 @@ export type ToolkitConfig = Record<string, ToolkitDefinition>;
41
41
  export interface Toolkit {
42
42
  readonly name: string;
43
43
  readonly description?: string;
44
+ /**
45
+ * The validated config this scope came from. Kept as immutable data so
46
+ * deployment-wide operator surfaces can explain the running configuration
47
+ * without inventing a second source of truth.
48
+ */
49
+ readonly connectors: readonly string[];
50
+ readonly includeTools: readonly string[];
51
+ readonly excludeTools: readonly string[];
44
52
  /** True when `connectorId` is inside this toolkit's scope. */
45
53
  hasConnector(connectorId: string): boolean;
46
54
  /** True when `<connectorId>.<toolName>` is inside this toolkit's scope. */
@@ -178,6 +186,9 @@ function resolveToolkit(
178
186
  return {
179
187
  name,
180
188
  ...(definition.description ? { description: definition.description } : {}),
189
+ connectors: Object.freeze([...connectorIds]),
190
+ includeTools: Object.freeze([...(definition.includeTools ?? [])]),
191
+ excludeTools: Object.freeze([...(definition.excludeTools ?? [])]),
181
192
  hasConnector: (connectorId) => connectorIds.has(connectorId),
182
193
  hasTool: (connectorId, toolName) => {
183
194
  if (!connectorIds.has(connectorId)) return false;
package/src/types.ts CHANGED
@@ -113,9 +113,9 @@ export interface ConnectorContext {
113
113
  * when omitted.
114
114
  */
115
115
  requestScope?: object;
116
- /** Best-effort cancellation signal for this individual tool call. */
116
+ /** Best-effort cancellation signal for this connector operation. */
117
117
  signal?: AbortSignal;
118
- /** Requested tool-call deadline in milliseconds. */
118
+ /** Requested connector-operation deadline in milliseconds. */
119
119
  timeoutMs?: number;
120
120
  }
121
121
 
@@ -264,6 +264,23 @@ export interface ExecutorProvider {
264
264
  */
265
265
  export interface Executor {
266
266
  execute(code: string, providers: ExecutorProvider[]): Promise<ExecuteResult>;
267
+ /** Release runtime resources. Node's built-in pool implements this. */
268
+ close?(): void | Promise<void>;
269
+ }
270
+
271
+ /**
272
+ * Optional admission capability used by bounded executors. The acquired lease
273
+ * carries execution so an already-admitted caller cannot accidentally acquire
274
+ * a second slot and deadlock a pool of one.
275
+ */
276
+ export interface AdmittingExecutor extends Executor {
277
+ acquire(options?: { signal?: AbortSignal }): Promise<ExecutorLease>;
278
+ }
279
+
280
+ export interface ExecutorLease {
281
+ execute(code: string, providers: ExecutorProvider[]): Promise<ExecuteResult>;
282
+ /** Idempotent. Call from finally even when provider construction fails. */
283
+ release(): void;
267
284
  }
268
285
 
269
286
  /**
package/src/ui.ts CHANGED
@@ -4,8 +4,12 @@ import {
4
4
  storedCredentialShape,
5
5
  } from "./credentials.js";
6
6
  import type { CredentialVault } from "./credentials.js";
7
- import { closeConnectorScope } from "./connector-scope.js";
7
+ import {
8
+ closeConnectorScope,
9
+ type DeferredWork,
10
+ } from "./connector-scope.js";
8
11
  import type { Registry } from "./registry.js";
12
+ import type { Toolkit } from "./toolkits.js";
9
13
  import type { ConnectaBranding, UiAuthConfig } from "./types.js";
10
14
 
11
15
  /** Connecta's default monochrome "C" mark. */
@@ -296,10 +300,21 @@ export interface UiConnector {
296
300
  export interface UiData {
297
301
  serverInfo: { name: string; version: string };
298
302
  connectors: UiConnector[];
303
+ /** Read-only projection of the validated deployment config. */
304
+ toolkits: UiToolkit[];
299
305
  activityEnabled: boolean;
300
306
  credentialManagement: CredentialManagementCapability;
301
307
  }
302
308
 
309
+ export interface UiToolkit {
310
+ name: string;
311
+ connectors: string[];
312
+ includeTools: string[];
313
+ excludeTools: string[];
314
+ /** Tools currently loaded through healthy connectors and visible in this view. */
315
+ toolCount: number;
316
+ }
317
+
303
318
  export type CredentialManagementCapability =
304
319
  | "available"
305
320
  | "requires_clerk"
@@ -391,10 +406,12 @@ export async function buildUiData(
391
406
  credentialManagement: CredentialManagementCapability = credentialVault
392
407
  ? "available"
393
408
  : "requires_clerk",
409
+ toolkits?: ReadonlyMap<string, Toolkit>,
410
+ defer?: DeferredWork,
394
411
  ): Promise<UiData> {
395
412
  const requestScope = {};
396
413
  const connectorSet = registry.listConnectors();
397
- const connectors = await Promise.all(
414
+ const settled = await Promise.allSettled(
398
415
  connectorSet.map(async (c): Promise<UiConnector> => {
399
416
  const status = await registry.statusFor(c.id, baseUrl, requestScope);
400
417
  const credentialCheck = await registry.credentialHealthFor(c.id);
@@ -530,19 +547,42 @@ export async function buildUiData(
530
547
  ...(credential ? { credential } : {}),
531
548
  };
532
549
  }),
533
- ).finally(async () => {
534
- await Promise.all(
535
- connectorSet.map((connector) =>
536
- closeConnectorScope(
537
- connector,
538
- registry.contextFor(connector.id, baseUrl, requestScope),
539
- ),
550
+ );
551
+ await Promise.all(
552
+ connectorSet.map((connector) =>
553
+ closeConnectorScope(
554
+ connector,
555
+ registry.contextFor(connector.id, baseUrl, requestScope),
556
+ defer,
540
557
  ),
541
- );
558
+ ),
559
+ );
560
+ const connectors = settled.map((result) => {
561
+ if (result.status === "rejected") throw result.reason;
562
+ return result.value;
542
563
  });
564
+ const toolkitData: UiToolkit[] = [...(toolkits?.values() ?? [])].map(
565
+ (toolkit) => ({
566
+ name: toolkit.name,
567
+ connectors: [...toolkit.connectors],
568
+ includeTools: [...toolkit.includeTools],
569
+ excludeTools: [...toolkit.excludeTools],
570
+ toolCount: connectors.reduce(
571
+ (count, connector) =>
572
+ count +
573
+ (toolkit.hasConnector(connector.id)
574
+ ? connector.tools.filter((tool) =>
575
+ toolkit.hasTool(connector.id, tool.name),
576
+ ).length
577
+ : 0),
578
+ 0,
579
+ ),
580
+ }),
581
+ );
543
582
  return {
544
583
  serverInfo,
545
584
  connectors,
585
+ toolkits: toolkitData,
546
586
  activityEnabled,
547
587
  credentialManagement,
548
588
  };
@@ -876,6 +916,56 @@ ${clerkScript}
876
916
  .connector-description { margin-top: .25rem; max-width: 40rem; }
877
917
  .connector-message,
878
918
  .connector-auth { margin-top: .75rem; }
919
+ .connector-toolkits {
920
+ align-items: baseline;
921
+ display: flex;
922
+ flex-wrap: wrap;
923
+ gap: .25rem .5rem;
924
+ margin-top: .5rem;
925
+ }
926
+ .scope-label {
927
+ border: 1px solid var(--rule);
928
+ display: inline-block;
929
+ font-family: var(--mono);
930
+ font-size: .72rem;
931
+ padding: .05rem .35rem;
932
+ }
933
+
934
+ .toolkit-copy { margin-bottom: 1rem; max-width: 42rem; }
935
+ .toolkit-ledger { border-bottom: 1px solid var(--rule); }
936
+ .toolkit-card {
937
+ border-top: 1px solid var(--rule);
938
+ padding: .9rem 0 1rem;
939
+ }
940
+ .toolkit-head {
941
+ display: grid;
942
+ gap: var(--gap);
943
+ grid-template-columns: minmax(0, 2fr) minmax(10rem, 1fr);
944
+ }
945
+ .toolkit-name {
946
+ align-items: baseline;
947
+ display: flex;
948
+ flex-wrap: wrap;
949
+ gap: .5rem;
950
+ }
951
+ .toolkit-name h3 { overflow-wrap: anywhere; }
952
+ .toolkit-state { text-align: right; }
953
+ .toolkit-connectors {
954
+ display: flex;
955
+ flex-wrap: wrap;
956
+ gap: .5rem;
957
+ margin-top: .75rem;
958
+ }
959
+ .toolkit-connector {
960
+ border-left: 1px solid var(--ink);
961
+ display: inline-flex;
962
+ flex-wrap: wrap;
963
+ gap: .4rem;
964
+ padding-left: .5rem;
965
+ }
966
+ .toolkit-rules { margin-top: .75rem; }
967
+ .toolkit-rules > * + * { margin-top: .25rem; }
968
+ .toolkit-endpoint { margin-top: .75rem; }
879
969
 
880
970
  .credential-ledger { border-bottom: 1px solid var(--rule); }
881
971
  .credential-card { border-top: 1px solid var(--rule); padding-bottom: .75rem; padding-top: .75rem; }
@@ -991,9 +1081,11 @@ ${clerkScript}
991
1081
  .section,
992
1082
  .section + .section { margin-top: 2.5rem; }
993
1083
  .connector-head,
1084
+ .toolkit-head,
994
1085
  .tool,
995
1086
  .activity-item { grid-template-columns: 1fr; }
996
- .connector-state { text-align: left; }
1087
+ .connector-state,
1088
+ .toolkit-state { text-align: left; }
997
1089
  .credential-field { align-items: start; grid-template-columns: 1fr; gap: .25rem; }
998
1090
  input { min-height: 2.75rem; }
999
1091
  }
@@ -1068,6 +1160,16 @@ ${clerkScript}
1068
1160
  <div id="list" class="connector-tools" aria-busy="false"></div>
1069
1161
  </div>
1070
1162
  </section>
1163
+ <section class="section pgrid" aria-labelledby="toolkitLedgerHeading">
1164
+ <h2 class="pcap" id="toolkitLedgerHeading">Toolkits</h2>
1165
+ <div class="pbody">
1166
+ <p class="toolkit-copy meta">
1167
+ Read-only views from deployment config. Change the config and redeploy
1168
+ to update them.
1169
+ </p>
1170
+ <div id="toolkitList" class="toolkit-ledger"></div>
1171
+ </div>
1172
+ </section>
1071
1173
  </section>
1072
1174
 
1073
1175
  <section id="credentialsView"${page === "credentials" ? "" : ' class="hidden"'}>
@@ -1129,6 +1231,7 @@ let ACTIVITY_LOADED = false;
1129
1231
  let CURRENT_PAGE = INITIAL_PAGE;
1130
1232
  let SESSION_GENERATION = 0;
1131
1233
  let ACTIVITY_GENERATION = 0;
1234
+ let CLERK_SESSION_ID = null;
1132
1235
  $("mcpUrl").textContent = MCP_URL;
1133
1236
 
1134
1237
  function esc(s) {
@@ -1184,6 +1287,7 @@ function clearIdentityState() {
1184
1287
  DATA = null;
1185
1288
  clearActivityState();
1186
1289
  $("list").innerHTML = "";
1290
+ $("toolkitList").innerHTML = "";
1187
1291
  $("filter").value = "";
1188
1292
  $("credentialList").innerHTML = "";
1189
1293
  $("credentialList").setAttribute("aria-busy", "false");
@@ -1267,7 +1371,10 @@ function activatePage(page, options) {
1267
1371
  loadActivity(true);
1268
1372
  }
1269
1373
  }
1270
- if (next !== "credentials") $("credentialList").innerHTML = "";
1374
+ if (next !== "credentials") {
1375
+ $("credentialList").innerHTML = "";
1376
+ setNotice("");
1377
+ }
1271
1378
  // Focus what is actually on screen. While gated the page views are hidden, so
1272
1379
  // focusing their heading is a silent no-op that drops focus to <body> and
1273
1380
  // restarts the next Tab from the top of the document — the gate's own h1 is
@@ -1452,6 +1559,71 @@ async function loadActivity(reset) {
1452
1559
  }
1453
1560
  }
1454
1561
 
1562
+ function toolkitMcpUrl(name) {
1563
+ const url = new URL(MCP_URL, window.location.href);
1564
+ url.searchParams.set("toolkit", name);
1565
+ return url.toString();
1566
+ }
1567
+
1568
+ function renderToolkits() {
1569
+ const list = $("toolkitList");
1570
+ const toolkits = DATA.toolkits || [];
1571
+ list.innerHTML = "";
1572
+ if (!toolkits.length) {
1573
+ list.innerHTML =
1574
+ '<p class="empty">No toolkits are configured. Unscoped clients see the full connector registry.</p>';
1575
+ return;
1576
+ }
1577
+ const connectors = new Map(
1578
+ DATA.connectors.map((connector) => [connector.id, connector]),
1579
+ );
1580
+ for (const toolkit of toolkits) {
1581
+ const el = document.createElement("article");
1582
+ el.className = "toolkit-card";
1583
+ const connectorCount = toolkit.connectors.length;
1584
+ const toolCount = toolkit.toolCount || 0;
1585
+ let body =
1586
+ '<div class="toolkit-head"><div><div class="toolkit-name">' +
1587
+ '<h3 class="mono">' + esc(toolkit.name) + "</h3>" +
1588
+ '<span class="cap">configured view</span></div>';
1589
+ body += '</div><div class="toolkit-state cap">' +
1590
+ connectorCount + (connectorCount === 1 ? " connector" : " connectors") +
1591
+ " · " + toolCount + (toolCount === 1 ? " loaded tool" : " loaded tools") +
1592
+ "</div></div>";
1593
+ body += '<ul class="toolkit-connectors" aria-label="Included connectors">';
1594
+ for (const id of toolkit.connectors) {
1595
+ const connector = connectors.get(id);
1596
+ body += '<li class="toolkit-connector"><span>' +
1597
+ esc(connector?.title || id) + '</span><code class="mono">' +
1598
+ esc(id) + "</code></li>";
1599
+ }
1600
+ body += "</ul>";
1601
+ body += '<div class="toolkit-rules meta">';
1602
+ if (toolkit.includeTools.length) {
1603
+ body += '<p>Per-connector allowlist: <span class="mono">' +
1604
+ toolkit.includeTools.map(esc).join('</span>, <span class="mono">') +
1605
+ "</span>. Connectors without an allowlist keep all tools.</p>";
1606
+ }
1607
+ if (toolkit.excludeTools.length) {
1608
+ body += '<p>Hidden: <span class="mono">' +
1609
+ toolkit.excludeTools.map(esc).join('</span>, <span class="mono">') +
1610
+ "</span></p>";
1611
+ }
1612
+ if (!toolkit.includeTools.length && !toolkit.excludeTools.length) {
1613
+ body += "<p>All tools on the included connectors are visible.</p>";
1614
+ }
1615
+ body += "</div>";
1616
+ const endpoint = toolkitMcpUrl(toolkit.name);
1617
+ body += '<div class="endpoint toolkit-endpoint"><div class="endpoint-row">' +
1618
+ '<code class="mono">' + esc(endpoint) + "</code>" +
1619
+ '<button class="linklike" type="button" data-toolkit-copy="' +
1620
+ esc(toolkit.name) + '" aria-label="Copy endpoint for ' +
1621
+ esc(toolkit.name) + '">Copy URL</button></div></div>';
1622
+ el.innerHTML = body;
1623
+ list.appendChild(el);
1624
+ }
1625
+ }
1626
+
1455
1627
  function renderConnections() {
1456
1628
  const q = $("filter").value.trim().toLowerCase();
1457
1629
  const list = $("list");
@@ -1472,6 +1644,15 @@ function renderConnections() {
1472
1644
  if (c.description) {
1473
1645
  head += '<p class="connector-description meta">' + esc(c.description) + "</p>";
1474
1646
  }
1647
+ const memberships = (DATA.toolkits || [])
1648
+ .filter((toolkit) => toolkit.connectors.includes(c.id))
1649
+ .map((toolkit) => toolkit.name);
1650
+ if (memberships.length) {
1651
+ head += '<div class="connector-toolkits meta"><span>Toolkits</span>' +
1652
+ memberships.map((name) =>
1653
+ '<span class="scope-label">' + esc(name) + "</span>"
1654
+ ).join("") + "</div>";
1655
+ }
1475
1656
  head += '</div><div class="connector-state cap">' + esc(status) + " · " +
1476
1657
  c.toolCount + (c.toolCount === 1 ? " tool" : " tools") +
1477
1658
  '<br><span class="mono">' + esc(c.id) + "</span></div></div>";
@@ -1525,6 +1706,7 @@ function renderConnections() {
1525
1706
  : "No connectors are declared in this deployment.") +
1526
1707
  "</p>";
1527
1708
  }
1709
+ renderToolkits();
1528
1710
  }
1529
1711
 
1530
1712
  function renderCredentials() {
@@ -1771,6 +1953,19 @@ $("copyMcpUrl").onclick = async () => {
1771
1953
  }
1772
1954
  window.setTimeout(() => { button.textContent = "Copy URL"; }, 1600);
1773
1955
  };
1956
+ $("toolkitList").onclick = async (event) => {
1957
+ const button = event.target.closest("[data-toolkit-copy]");
1958
+ if (!button) return;
1959
+ try {
1960
+ await navigator.clipboard.writeText(
1961
+ toolkitMcpUrl(button.dataset.toolkitCopy),
1962
+ );
1963
+ button.textContent = "Copied";
1964
+ } catch (e) {
1965
+ button.textContent = "Copy failed";
1966
+ }
1967
+ window.setTimeout(() => { button.textContent = "Copy URL"; }, 1600);
1968
+ };
1774
1969
  $("signin").onclick = () => Clerk.redirectToSignIn({
1775
1970
  signInFallbackRedirectUrl: window.location.href,
1776
1971
  signUpFallbackRedirectUrl: window.location.href,
@@ -1820,6 +2015,17 @@ async function init() {
1820
2015
  signUpFallbackRedirectUrl: window.location.href,
1821
2016
  afterSignOutUrl: window.location.href,
1822
2017
  });
2018
+ CLERK_SESSION_ID = Clerk.session?.id ?? null;
2019
+ Clerk.addListener((resources) => {
2020
+ const nextSessionId = resources.session?.id ?? null;
2021
+ if (nextSessionId === CLERK_SESSION_ID) return;
2022
+ CLERK_SESSION_ID = nextSessionId;
2023
+ // Clerk has already updated its public session before notifying
2024
+ // listeners. Clear synchronously so stale identity-scoped data cannot
2025
+ // be repainted while the replacement identity is being fetched.
2026
+ showGate("");
2027
+ void load();
2028
+ });
1823
2029
  } catch (e) {
1824
2030
  return showGate("Clerk could not initialize: " + e.message);
1825
2031
  }
package/src/version.ts CHANGED
@@ -4,4 +4,4 @@
4
4
  * a bump that forgets this file fails the build rather than shipping a stale
5
5
  * version to `/health` and to downstream MCP handshakes.
6
6
  */
7
- export const CONNECTA_VERSION = "0.7.1";
7
+ export const CONNECTA_VERSION = "0.7.4";