@zackbart/connecta 0.4.1 → 0.6.0

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 (85) hide show
  1. package/CHANGELOG.md +527 -0
  2. package/README.md +83 -7
  3. package/SECURITY.md +10 -6
  4. package/dist/activity.d.ts +8 -0
  5. package/dist/activity.d.ts.map +1 -1
  6. package/dist/activity.js +1 -0
  7. package/dist/activity.js.map +1 -1
  8. package/dist/auth/bearer.d.ts +10 -3
  9. package/dist/auth/bearer.d.ts.map +1 -1
  10. package/dist/auth/bearer.js +21 -0
  11. package/dist/auth/bearer.js.map +1 -1
  12. package/dist/auth/clerk.d.ts +26 -1
  13. package/dist/auth/clerk.d.ts.map +1 -1
  14. package/dist/auth/clerk.js +161 -4
  15. package/dist/auth/clerk.js.map +1 -1
  16. package/dist/connectors/api.d.ts +13 -0
  17. package/dist/connectors/api.d.ts.map +1 -1
  18. package/dist/connectors/api.js +2 -0
  19. package/dist/connectors/api.js.map +1 -1
  20. package/dist/connectors/remote-mcp.d.ts +13 -0
  21. package/dist/connectors/remote-mcp.d.ts.map +1 -1
  22. package/dist/connectors/remote-mcp.js +10 -0
  23. package/dist/connectors/remote-mcp.js.map +1 -1
  24. package/dist/credential-health.d.ts +212 -0
  25. package/dist/credential-health.d.ts.map +1 -0
  26. package/dist/credential-health.js +535 -0
  27. package/dist/credential-health.js.map +1 -0
  28. package/dist/execute.d.ts +4 -4
  29. package/dist/execute.d.ts.map +1 -1
  30. package/dist/execute.js +16 -4
  31. package/dist/execute.js.map +1 -1
  32. package/dist/index.d.ts +77 -2
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +112 -2
  35. package/dist/index.js.map +1 -1
  36. package/dist/meta-tools.d.ts +76 -7
  37. package/dist/meta-tools.d.ts.map +1 -1
  38. package/dist/meta-tools.js +328 -98
  39. package/dist/meta-tools.js.map +1 -1
  40. package/dist/registry.d.ts +245 -2
  41. package/dist/registry.d.ts.map +1 -1
  42. package/dist/registry.js +377 -27
  43. package/dist/registry.js.map +1 -1
  44. package/dist/server.d.ts +7 -1
  45. package/dist/server.d.ts.map +1 -1
  46. package/dist/server.js +342 -27
  47. package/dist/server.js.map +1 -1
  48. package/dist/skills.d.ts +53 -2
  49. package/dist/skills.d.ts.map +1 -1
  50. package/dist/skills.js +162 -2
  51. package/dist/skills.js.map +1 -1
  52. package/dist/timeout.d.ts +16 -0
  53. package/dist/timeout.d.ts.map +1 -0
  54. package/dist/timeout.js +38 -0
  55. package/dist/timeout.js.map +1 -0
  56. package/dist/toolkits.d.ts +138 -0
  57. package/dist/toolkits.d.ts.map +1 -0
  58. package/dist/toolkits.js +319 -0
  59. package/dist/toolkits.js.map +1 -0
  60. package/dist/types.d.ts +90 -1
  61. package/dist/types.d.ts.map +1 -1
  62. package/dist/ui.d.ts +63 -0
  63. package/dist/ui.d.ts.map +1 -1
  64. package/dist/ui.js +176 -11
  65. package/dist/ui.js.map +1 -1
  66. package/dist/version.d.ts +1 -1
  67. package/dist/version.js +1 -1
  68. package/package.json +5 -2
  69. package/src/activity.ts +9 -0
  70. package/src/auth/bearer.ts +35 -1
  71. package/src/auth/clerk.ts +202 -5
  72. package/src/connectors/api.ts +15 -0
  73. package/src/connectors/remote-mcp.ts +24 -0
  74. package/src/credential-health.ts +736 -0
  75. package/src/execute.ts +32 -8
  76. package/src/index.ts +226 -2
  77. package/src/meta-tools.ts +397 -119
  78. package/src/registry.ts +540 -29
  79. package/src/server.ts +431 -25
  80. package/src/skills.ts +185 -2
  81. package/src/timeout.ts +49 -0
  82. package/src/toolkits.ts +450 -0
  83. package/src/types.ts +96 -2
  84. package/src/ui.ts +190 -11
  85. package/src/version.ts +1 -1
package/src/execute.ts CHANGED
@@ -2,10 +2,15 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
3
  import { compactSchema, rankTools, summarizeDescription } from "./catalog.js";
4
4
  import { recordToolActivity, type ActivityRequestContext } from "./activity.js";
5
- import { errorResult, jsonResult, type ToolResult } from "./meta-tools.js";
5
+ import {
6
+ errorResult,
7
+ jsonResult,
8
+ serializeResultText,
9
+ type ToolResult,
10
+ } from "./meta-tools.js";
6
11
  import { classifyCallError, ConnectorCallError } from "./errors.js";
7
12
  import { unwrapMcpResult } from "./mcp-result.js";
8
- import type { Registry } from "./registry.js";
13
+ import type { RegistryView } from "./registry.js";
9
14
  import type {
10
15
  Connector,
11
16
  Executor,
@@ -98,7 +103,7 @@ export function unwrapForSandbox(
98
103
  * the raw-address escape hatch. Broken connectors are skipped, not fatal.
99
104
  */
100
105
  export async function buildSandboxProviders(
101
- registry: Registry,
106
+ registry: RegistryView,
102
107
  baseUrl: string,
103
108
  logger: Logger,
104
109
  activity?: ActivityRequestContext,
@@ -132,6 +137,7 @@ export async function buildSandboxProviders(
132
137
  "__log",
133
138
  ]);
134
139
  const connectors = registry.listConnectors();
140
+ const catalogStarted = Date.now();
135
141
  const loaded = await Promise.allSettled(
136
142
  connectors.map((connector) =>
137
143
  registry.getTools(connector.id, baseUrl, requestScope),
@@ -249,8 +255,25 @@ export async function buildSandboxProviders(
249
255
  }
250
256
  const loadedTools = loaded[i];
251
257
  if (loadedTools.status === "rejected") {
258
+ // Same health accounting as the call_tool catalog catch: a connector whose
259
+ // catalog cannot be fetched is unusable, and dropping its namespace with
260
+ // only a warn would leave the cheap `list_connectors({ probe: false })`
261
+ // signal clean for a code-mode deployment whose downstream grant was
262
+ // revoked. Recorded through `registry` — this run's view — so a
263
+ // toolkit-scoped execute_code lands in that toolkit's log as well.
264
+ registry.recordFailure(
265
+ connector.id,
266
+ Date.now() - catalogStarted,
267
+ loadedTools.reason,
268
+ );
269
+ // classifyCallError so a typed auth_required thrown while listing tools
270
+ // keeps its code where an operator can see it; health stores the message.
271
+ const details = classifyCallError(
272
+ loadedTools.reason,
273
+ "catalog_lookup_failed",
274
+ );
252
275
  logger.warn(
253
- `[connecta] execute_code: connector "${connector.id}" skipped: ${msg(loadedTools.reason)}`,
276
+ `[connecta] execute_code: connector "${connector.id}" skipped (${details.code}): ${msg(loadedTools.reason)}`,
254
277
  );
255
278
  continue;
256
279
  }
@@ -446,8 +469,9 @@ function truncate(text: string, max: number): string {
446
469
  }
447
470
 
448
471
  function guardResultValue(value: unknown): unknown {
449
- const serialized = JSON.stringify(value, null, 2);
450
- const text = serialized === undefined ? String(value) : serialized;
472
+ // Same serialization the call_tool guards measure, so a program returning
473
+ // nothing is rendered one way across every result path (issue #42).
474
+ const text = serializeResultText(value);
451
475
  if (text.length <= MAX_RESULT_CHARS) return value;
452
476
  return {
453
477
  truncated: true,
@@ -459,7 +483,7 @@ function guardResultValue(value: unknown): unknown {
459
483
 
460
484
  /** The execute_code handler. Exported for direct testing. */
461
485
  export function createExecuteTool(
462
- registry: Registry,
486
+ registry: RegistryView,
463
487
  baseUrl: string,
464
488
  executor: Executor,
465
489
  logger: Logger,
@@ -527,7 +551,7 @@ Example: async () => { const r = await crm.search({ query: "roadmap" }); return
527
551
  /** Register the execute_code meta-tool. Only called when an executor is configured. */
528
552
  export function registerExecuteTool(
529
553
  server: McpServer,
530
- registry: Registry,
554
+ registry: RegistryView,
531
555
  ctx: {
532
556
  baseUrl: string;
533
557
  executor: Executor;
package/src/index.ts CHANGED
@@ -1,9 +1,20 @@
1
1
  import { CredentialVault } from "./credentials.js";
2
2
  import { Registry } from "./registry.js";
3
3
  import { createFetchHandler } from "./server.js";
4
+ import { droppedBrandingUrls, droppedUiAuthUrls } from "./ui.js";
5
+ import {
6
+ resolveToolkits,
7
+ validateToolkitBindings,
8
+ type Toolkit,
9
+ type ToolkitConfig,
10
+ } from "./toolkits.js";
4
11
  import { memoryStorage } from "./storage/memory.js";
5
12
  import { CONNECTA_VERSION } from "./version.js";
6
13
  import type { ActivityReadGate, ActivityStore } from "./activity.js";
14
+ import type {
15
+ CredentialCheckResult,
16
+ CredentialHealthConfig,
17
+ } from "./credential-health.js";
7
18
  import type {
8
19
  Connector,
9
20
  ConnectaBranding,
@@ -15,6 +26,38 @@ import type {
15
26
 
16
27
  export interface ConnectaConfig {
17
28
  connectors: Connector[];
29
+ /**
30
+ * Named scoped views over `connectors`, selected per client connection with
31
+ * `?toolkit=<name>` on the `/mcp` URL. One deployment belongs to one org;
32
+ * a toolkit is the slice of it a group of team members sees.
33
+ *
34
+ * ```ts
35
+ * toolkits: {
36
+ * support: { connectors: ["zendesk", "notion"] },
37
+ * exec: {
38
+ * connectors: ["zendesk", "notion", "gmail"],
39
+ * excludeTools: ["gmail.send_message"],
40
+ * },
41
+ * }
42
+ * ```
43
+ *
44
+ * Inside a toolkit-scoped session every meta-tool behaves as if out-of-scope
45
+ * connectors and tools do not exist, and an out-of-scope address fails
46
+ * exactly as a nonexistent one does. No `?toolkit=` ⇒ the full registry, so
47
+ * adding toolkits changes nothing for connections that don't ask for one; an
48
+ * unknown name is an error, never a silent fallback.
49
+ *
50
+ * Selection is self-service until a toolkit is BOUND to an inbound identity:
51
+ * pass `toolkits` to an auth adapter — `bearerToken(secret, { toolkits:
52
+ * ["support"] })` — and that credential may open only those toolkits, and may
53
+ * not connect unscoped unless it also passes `unscoped: true`. An unbound
54
+ * identity keeps the self-service behavior.
55
+ *
56
+ * Definitions are validated at construction: an unknown connector id, an
57
+ * empty connector selection, an empty `includeTools`, a malformed tool
58
+ * address, or an address naming no tool on an in-code connector all throw.
59
+ */
60
+ toolkits?: ToolkitConfig;
18
61
  /**
19
62
  * Privacy-minimal downstream tool activity storage. Writes are best-effort
20
63
  * and never change tool results. Implement `list` to enable the Activity UI.
@@ -58,7 +101,9 @@ export interface ConnectaConfig {
58
101
  toolCatalogStaleSeconds?: number;
59
102
  /**
60
103
  * Max inline result size (bytes) before call_tool/batch_call truncate and
61
- * stash the full text for get_result paging. Default 50_000.
104
+ * stash the full text for get_result paging. Must be a whole number of bytes
105
+ * >= 1; anything else (0, negative, fractional, NaN, Infinity) warns at
106
+ * startup and falls back to the default 50_000.
62
107
  */
63
108
  maxResultBytes?: number;
64
109
  /**
@@ -93,6 +138,19 @@ export interface ConnectaConfig {
93
138
  * real cancellation of the downstream request is a deferred follow-up.
94
139
  */
95
140
  probeTimeoutMs?: number;
141
+ /**
142
+ * Tuning for the proactive credential liveness checks (issue #24) that let a
143
+ * connector's status flip to `auth_required` *before* an agent's call fails.
144
+ * Defaults are safe to leave alone: at most one check per connector per 15
145
+ * minutes, four in flight, 30 s each, triggered opportunistically by inbound
146
+ * authenticated traffic. Only connectors holding a credential connecta stores
147
+ * — an operator-managed `credential`, or a downstream-OAuth grant — are ever
148
+ * checked, and a check never calls a downstream tool.
149
+ *
150
+ * `Connecta.checkCredentials()` is the same check on demand, for a Worker cron
151
+ * trigger or a Node interval.
152
+ */
153
+ credentialHealth?: CredentialHealthConfig;
96
154
  serverInfo?: {
97
155
  name?: string;
98
156
  version?: string;
@@ -118,6 +176,29 @@ export interface Connecta {
118
176
  /** Web-standard fetch handler. Usable as `export default { fetch: connecta.fetch }`. */
119
177
  fetch: (request: Request, env?: unknown, ctx?: unknown) => Promise<Response>;
120
178
  registry: Registry;
179
+ /**
180
+ * Check the stored downstream credentials now — the scheduler-facing half of
181
+ * credential health (issue #24). Wire it to whatever timer the runtime has:
182
+ *
183
+ * ```ts
184
+ * // Cloudflare Workers (wrangler.jsonc: "triggers": { "crons": ["*\/15 * * * *"] })
185
+ * async scheduled(_c, env, ctx) { ctx.waitUntil(build(env).checkCredentials()); }
186
+ * // Node
187
+ * setInterval(() => void connecta.checkCredentials(), 15 * 60_000).unref();
188
+ * ```
189
+ *
190
+ * Returns one outcome per connector considered, including why a connector was
191
+ * skipped (`fresh` is the rate limit: a connector checked less than
192
+ * `credentialHealth.intervalSeconds` ago is not re-checked unless `force`).
193
+ * Never rejects on a connector failure — a broken connector becomes an `error`
194
+ * verdict. Needs a base URL for connector contexts: `publicUrl` supplies it,
195
+ * or pass one.
196
+ */
197
+ checkCredentials: (opts?: {
198
+ baseUrl?: string;
199
+ force?: boolean;
200
+ ids?: string[];
201
+ }) => Promise<CredentialCheckResult[]>;
121
202
  }
122
203
 
123
204
  function defaultLogger(): Logger {
@@ -146,6 +227,7 @@ function normalizeAuth(auth: ConnectaConfig["auth"]): InboundAuth[] {
146
227
  function warnInsecureConfig(
147
228
  config: ConnectaConfig,
148
229
  inboundAuth: InboundAuth[],
230
+ toolkits: ReadonlyMap<string, Toolkit> | undefined,
149
231
  logger: Logger,
150
232
  ): void {
151
233
  const oauthConnectors = config.connectors.filter((c) => c.finishAuth);
@@ -176,6 +258,95 @@ function warnInsecureConfig(
176
258
  );
177
259
  }
178
260
 
261
+ // Toolkits that nothing binds to an identity: selection is then self-service,
262
+ // and the boundary organizes the surface rather than protecting it. Three
263
+ // distinct shapes, so three distinct warnings — an operator can only act on
264
+ // the one they are actually in.
265
+ //
266
+ // All are keyed off the RESOLVED toolkits, which is the same map `?toolkit=`
267
+ // resolves against, rather than the presence of the config key: `toolkits: {}`
268
+ // is a truthy object that resolves to nothing selectable, so warning about a
269
+ // choice no caller can make would name a risk that does not exist.
270
+ if (toolkits) {
271
+ const unbound = inboundAuth.filter((provider) => !provider.toolkitBinding);
272
+ if (inboundAuth.length === 0) {
273
+ // No auth at all ⇒ no identity exists to bind, so binding is not even the
274
+ // fix here. The open-mode warning above covers the wider exposure.
275
+ logger.warn(
276
+ "[connecta] toolkits are configured but there is no inbound " +
277
+ "authentication: with no identity to bind a toolkit to, any caller " +
278
+ "can choose any toolkit or omit ?toolkit= and see every connector. " +
279
+ "Configure `auth` (for example bearerToken(...) or Clerk), then bind " +
280
+ "each credential with `toolkits: [...]`.",
281
+ );
282
+ } else if (unbound.length === inboundAuth.length) {
283
+ // Authenticated, but every credential may still select every view. This is
284
+ // the shape issue #37 exists to close, and it is invisible without a line
285
+ // saying so: nothing fails, the teams are simply not separated.
286
+ logger.warn(
287
+ "[connecta] toolkits are configured but no inbound identity is bound " +
288
+ "to one: every credential `auth` admits can select any toolkit, or " +
289
+ "omit ?toolkit= and see the whole deployment, so a token handed to " +
290
+ "one team also opens the others' views. Bind each credential with " +
291
+ "`toolkits: [...]` on its auth adapter (add `unscoped: true` for an " +
292
+ "operator credential that should still see everything).",
293
+ );
294
+ } else if (unbound.length > 0) {
295
+ // The dangerous middle: SOME credentials are bound, which is exactly when
296
+ // an operator believes the deployment is separated — while one forgotten
297
+ // provider still opens every view and the whole deployment-wide surface.
298
+ // Naming the unbound providers is the point; an intentionally unrestricted
299
+ // credential says so with `unscoped: true` and stops appearing here.
300
+ const counted = new Map<string, number>();
301
+ for (const provider of unbound) {
302
+ counted.set(provider.kind, (counted.get(provider.kind) ?? 0) + 1);
303
+ }
304
+ const named = [...counted]
305
+ .map(([kind, count]) => (count > 1 ? `${kind} x${count}` : kind))
306
+ .join(", ");
307
+ logger.warn(
308
+ `[connecta] toolkits are bound on some inbound auth providers but not ` +
309
+ `all: ${named} ${unbound.length === 1 ? "declares" : "declare"} no ` +
310
+ "binding, so a caller that provider admits can still select any " +
311
+ "toolkit, connect unscoped, and read the deployment-wide operator " +
312
+ "surfaces — whatever the bound credentials beside it allow. Bind it " +
313
+ "too, or declare the exemption with `toolkits: [...], unscoped: true` " +
314
+ "if it is meant to be an operator credential.",
315
+ );
316
+ }
317
+ }
318
+
319
+ // Branding URLs that failed their scheme gate. Rendering silently falls back
320
+ // (a bad URL must not take the page down), so this warning is the only way an
321
+ // operator learns their value never reached the page.
322
+ const dropped = droppedBrandingUrls(config.branding);
323
+ if (dropped.length > 0) {
324
+ logger.warn(
325
+ `[connecta] branding ${dropped.join(", ")} dropped: a branding URL is ` +
326
+ "used as an href, so it must be an absolute http(s) URL (favicon.href " +
327
+ "may also be a root-relative path). The default is rendered instead.",
328
+ );
329
+ }
330
+
331
+ // /ui renders exactly one provider's browser sign-in config — the first that
332
+ // offers one, which is the same `find` the /ui route performs — and that
333
+ // provider's frontendApiUrl becomes the loader's `<script src>`. Gate-or-drop
334
+ // like a branding href: rendering omits the loader for a rejected value and
335
+ // the dashboard then reports that Clerk could not load, a confusing symptom
336
+ // without this line naming the cause. Checking only the rendered provider
337
+ // keeps the claim true — a later provider's uiAuth never reaches the page, so
338
+ // there is nothing there to warn about.
339
+ const uiAuthProvider = inboundAuth.find((provider) => provider.uiAuth);
340
+ const droppedUiAuth = droppedUiAuthUrls(uiAuthProvider?.uiAuth);
341
+ if (uiAuthProvider && droppedUiAuth.length > 0) {
342
+ logger.warn(
343
+ `[connecta] inbound auth provider "${uiAuthProvider.kind}" had ` +
344
+ `${droppedUiAuth.join(", ")} dropped: the browser sign-in loader is ` +
345
+ "fetched from this origin, so it must be an absolute https URL. /ui " +
346
+ "renders without the loader and cannot start a sign-in.",
347
+ );
348
+ }
349
+
179
350
  // OAuth connectors whose callback performs no state/CSRF check: the public
180
351
  // /oauth/callback/<id> route would exchange any delivered code.
181
352
  for (const connector of oauthConnectors) {
@@ -210,9 +381,19 @@ export function createConnecta(config: ConnectaConfig): Connecta {
210
381
  persistToolCatalog: config.persistToolCatalog,
211
382
  toolCatalogStaleSeconds: config.toolCatalogStaleSeconds,
212
383
  maxResultBytes: config.maxResultBytes,
384
+ credentialHealth: config.credentialHealth,
213
385
  });
386
+ // Throws on every structural mistake it can see (see resolveToolkits): a
387
+ // typo must not become a scope the operator never wrote. Note this is about
388
+ // the scope being *intended*, not about it being an access check — a toolkit
389
+ // scopes visibility, and `auth` remains the thing deciding who gets in.
390
+ const toolkits = resolveToolkits(config.toolkits, config.connectors);
214
391
  const inboundAuth = normalizeAuth(config.auth);
215
- warnInsecureConfig(config, inboundAuth, logger);
392
+ // Same contract for the identity half: a binding that names a toolkit this
393
+ // deployment does not declare would deny that credential every connection,
394
+ // with a 403 its client reports as a transport failure. Throw here instead.
395
+ validateToolkitBindings(inboundAuth, toolkits);
396
+ warnInsecureConfig(config, inboundAuth, toolkits, logger);
216
397
  const handler = createFetchHandler({
217
398
  registry,
218
399
  auth: inboundAuth,
@@ -232,6 +413,7 @@ export function createConnecta(config: ConnectaConfig): Connecta {
232
413
  credentialVault,
233
414
  deploymentInfo: config.deploymentInfo,
234
415
  branding: config.branding,
416
+ ...(toolkits ? { toolkits } : {}),
235
417
  });
236
418
  return {
237
419
  fetch: (request, _env, ctx) =>
@@ -242,6 +424,29 @@ export function createConnecta(config: ConnectaConfig): Connecta {
242
424
  : undefined,
243
425
  ),
244
426
  registry,
427
+ checkCredentials: (opts = {}) => {
428
+ // A scheduled check has no inbound request to derive an origin from, and
429
+ // a connector context without one would mint OAuth redirect URIs against
430
+ // a guess. Say so instead: the fix is one config line.
431
+ const baseUrl = opts.baseUrl ?? config.publicUrl;
432
+ if (!baseUrl) {
433
+ // Rejected, not thrown: the callers this is written for are
434
+ // `ctx.waitUntil(...)` and `.catch(...)` on the returned promise, and a
435
+ // synchronous throw escapes both — it would take down a scheduled
436
+ // handler instead of being reported by it.
437
+ return Promise.reject(
438
+ new Error(
439
+ "checkCredentials() needs a base URL: set `publicUrl` on the " +
440
+ "config (recommended — it is also what downstream OAuth " +
441
+ "callbacks use) or pass checkCredentials({ baseUrl }).",
442
+ ),
443
+ );
444
+ }
445
+ return registry.checkCredentialHealth(baseUrl, {
446
+ ...(opts.force !== undefined ? { force: opts.force } : {}),
447
+ ...(opts.ids ? { ids: opts.ids } : {}),
448
+ });
449
+ },
245
450
  };
246
451
  }
247
452
 
@@ -255,12 +460,30 @@ export type { ConnectorCallErrorCode, CallErrorDetails } from "./errors.js";
255
460
  export { validateToolInput } from "./validate.js";
256
461
  export type { ValidateToolInputOptions } from "./validate.js";
257
462
  export { bearerToken } from "./auth/bearer.js";
463
+ export type { BearerTokenOptions } from "./auth/bearer.js";
258
464
  export { memoryStorage } from "./storage/memory.js";
259
465
  export { CONNECTA_VERSION } from "./version.js";
260
466
  // Registry is reachable through `Connecta.registry`, so its type is public;
261
467
  // the class itself, the credential vault, and the meta-tool/sandbox factories
262
468
  // are internal factoring and are deliberately not part of the API surface.
263
469
  export type { Registry } from "./registry.js";
470
+ // Config-as-code shapes for `ConnectaConfig.toolkits` and the identity bindings
471
+ // that gate them. The resolved `Toolkit` and the `ScopedRegistry` that enforces
472
+ // it are internal factoring.
473
+ export type {
474
+ ToolkitBindingOptions,
475
+ ToolkitConfig,
476
+ ToolkitDefinition,
477
+ } from "./toolkits.js";
478
+ // Credential health: the config shape, and the result shape a scheduled
479
+ // `checkCredentials()` returns. The checker itself is internal factoring.
480
+ export type {
481
+ CredentialCheckResult,
482
+ CredentialCheckSkip,
483
+ CredentialCheckState,
484
+ CredentialHealthConfig,
485
+ CredentialHealthRecord,
486
+ } from "./credential-health.js";
264
487
 
265
488
  export type { RemoteMcpOptions, RemoteMcpAuth } from "./connectors/remote-mcp.js";
266
489
  export type { ApiOptions, ApiTool } from "./connectors/api.js";
@@ -278,6 +501,7 @@ export type {
278
501
  Executor,
279
502
  ExecutorProvider,
280
503
  InboundAuth,
504
+ ToolkitBinding,
281
505
  UiAuthConfig,
282
506
  AuthResult,
283
507
  JsonSchema,