@zackbart/connecta 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +505 -0
  2. package/README.md +159 -267
  3. package/dist/auth/bearer.d.ts +10 -3
  4. package/dist/auth/bearer.d.ts.map +1 -1
  5. package/dist/auth/bearer.js +21 -0
  6. package/dist/auth/bearer.js.map +1 -1
  7. package/dist/auth/clerk.d.ts +28 -3
  8. package/dist/auth/clerk.d.ts.map +1 -1
  9. package/dist/auth/clerk.js +161 -4
  10. package/dist/auth/clerk.js.map +1 -1
  11. package/dist/connectors/remote-mcp.d.ts.map +1 -1
  12. package/dist/connectors/remote-mcp.js +8 -0
  13. package/dist/connectors/remote-mcp.js.map +1 -1
  14. package/dist/credential-health.d.ts +220 -0
  15. package/dist/credential-health.d.ts.map +1 -0
  16. package/dist/credential-health.js +551 -0
  17. package/dist/credential-health.js.map +1 -0
  18. package/dist/credentials.d.ts +35 -1
  19. package/dist/credentials.d.ts.map +1 -1
  20. package/dist/credentials.js +42 -0
  21. package/dist/credentials.js.map +1 -1
  22. package/dist/execute.d.ts.map +1 -1
  23. package/dist/execute.js +16 -4
  24. package/dist/execute.js.map +1 -1
  25. package/dist/index.d.ts +46 -5
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +118 -15
  28. package/dist/index.js.map +1 -1
  29. package/dist/meta-tools.d.ts +56 -5
  30. package/dist/meta-tools.d.ts.map +1 -1
  31. package/dist/meta-tools.js +249 -92
  32. package/dist/meta-tools.js.map +1 -1
  33. package/dist/registry.d.ts +62 -0
  34. package/dist/registry.d.ts.map +1 -1
  35. package/dist/registry.js +85 -1
  36. package/dist/registry.js.map +1 -1
  37. package/dist/server.d.ts.map +1 -1
  38. package/dist/server.js +305 -40
  39. package/dist/server.js.map +1 -1
  40. package/dist/skills.d.ts +1 -1
  41. package/dist/skills.d.ts.map +1 -1
  42. package/dist/skills.js +3 -3
  43. package/dist/skills.js.map +1 -1
  44. package/dist/timeout.d.ts +16 -0
  45. package/dist/timeout.d.ts.map +1 -0
  46. package/dist/timeout.js +38 -0
  47. package/dist/timeout.js.map +1 -0
  48. package/dist/toolkits.d.ts +95 -1
  49. package/dist/toolkits.d.ts.map +1 -1
  50. package/dist/toolkits.js +190 -5
  51. package/dist/toolkits.js.map +1 -1
  52. package/dist/types.d.ts +81 -0
  53. package/dist/types.d.ts.map +1 -1
  54. package/dist/ui.d.ts +52 -0
  55. package/dist/ui.d.ts.map +1 -1
  56. package/dist/ui.js +144 -13
  57. package/dist/ui.js.map +1 -1
  58. package/dist/version.d.ts +1 -1
  59. package/dist/version.js +1 -1
  60. package/package.json +1 -1
  61. package/src/auth/bearer.ts +35 -1
  62. package/src/auth/clerk.ts +204 -7
  63. package/src/connectors/remote-mcp.ts +9 -0
  64. package/src/credential-health.ts +753 -0
  65. package/src/credentials.ts +71 -1
  66. package/src/execute.ts +28 -4
  67. package/src/index.ts +204 -22
  68. package/src/meta-tools.ts +286 -109
  69. package/src/registry.ts +125 -1
  70. package/src/server.ts +366 -38
  71. package/src/skills.ts +3 -3
  72. package/src/timeout.ts +49 -0
  73. package/src/toolkits.ts +241 -6
  74. package/src/types.ts +87 -1
  75. package/src/ui.ts +156 -14
  76. package/src/version.ts +1 -1
@@ -1,4 +1,8 @@
1
- import type { ConnectorCredentialValues, KVStorage } from "./types.js";
1
+ import type {
2
+ Connector,
3
+ ConnectorCredentialValues,
4
+ KVStorage,
5
+ } from "./types.js";
2
6
 
3
7
  const KEY_BYTES = 32;
4
8
  const IV_BYTES = 12;
@@ -35,6 +39,72 @@ export interface CredentialMetadata {
35
39
  fields?: Record<string, CredentialFieldMetadata>;
36
40
  }
37
41
 
42
+ /** Which hook a testable credential is checked with. */
43
+ export type CredentialTestMode = "single" | "multiple";
44
+
45
+ /** A declared credential shape whose only test hook cannot test it. */
46
+ export interface CredentialTestMismatch {
47
+ /** The shape the connector declared. */
48
+ shape: CredentialTestMode;
49
+ /** The hook it implements, which that shape cannot use. */
50
+ hook: "testCredential" | "testCredentials";
51
+ }
52
+
53
+ export interface CredentialTestRule {
54
+ /** The hook to call, or null when this credential cannot be tested at all. */
55
+ mode: CredentialTestMode | null;
56
+ /** Set only when the sole implemented hook is the one the shape cannot use. */
57
+ mismatch?: CredentialTestMismatch;
58
+ }
59
+
60
+ /**
61
+ * The one rule deciding whether a connector's credential can be tested — read
62
+ * by /ui's `testable` flag, by the `POST /ui/credentials/<id>/test` route when
63
+ * it picks a hook, and by the construction-time mismatch warning, so those
64
+ * three cannot drift apart.
65
+ *
66
+ * The declared credential *shape* selects the hook: named `credential.fields`
67
+ * are tested as a set by `testCredentials`, a single-value `credential` by
68
+ * `testCredential` on the vault's reserved `value` field. The other hook is
69
+ * never substituted — it would be handed a shape the connector never declared —
70
+ * so a connector implementing only the mismatched hook is not testable, and
71
+ * says so at construction rather than under an operator's click.
72
+ */
73
+ export function credentialTestRule(
74
+ connector: Pick<
75
+ Connector,
76
+ "credential" | "testCredential" | "testCredentials"
77
+ >,
78
+ ): CredentialTestRule {
79
+ if (!connector.credential) return { mode: null };
80
+ if (connector.credential.fields?.length) {
81
+ if (connector.testCredentials) return { mode: "multiple" };
82
+ return connector.testCredential
83
+ ? { mode: null, mismatch: { shape: "multiple", hook: "testCredential" } }
84
+ : { mode: null };
85
+ }
86
+ if (connector.testCredential) return { mode: "single" };
87
+ return connector.testCredentials
88
+ ? { mode: null, mismatch: { shape: "single", hook: "testCredentials" } }
89
+ : { mode: null };
90
+ }
91
+
92
+ /**
93
+ * One clause naming a mismatch, shared by the startup warning and the test
94
+ * route's 400 so an operator reads the same explanation in both places.
95
+ */
96
+ export function describeCredentialTestMismatch(
97
+ mismatch: CredentialTestMismatch,
98
+ ): string {
99
+ return mismatch.shape === "multiple"
100
+ ? "it declares named credential fields, which only " +
101
+ "`testCredentials(values, ctx)` can test, but implements " +
102
+ "`testCredential`"
103
+ : "it declares a single-value credential, which only " +
104
+ "`testCredential(value, ctx)` can test, but implements " +
105
+ "`testCredentials`";
106
+ }
107
+
38
108
  function storageKey(connectorId: string): string {
39
109
  return `conn:${connectorId}:credential:v1`;
40
110
  }
package/src/execute.ts CHANGED
@@ -2,7 +2,12 @@ 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
13
  import type { RegistryView } from "./registry.js";
@@ -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,
package/src/index.ts CHANGED
@@ -1,11 +1,24 @@
1
- import { CredentialVault } from "./credentials.js";
1
+ import {
2
+ CredentialVault,
3
+ credentialTestRule,
4
+ describeCredentialTestMismatch,
5
+ } from "./credentials.js";
2
6
  import { Registry } from "./registry.js";
3
7
  import { createFetchHandler } from "./server.js";
4
- import { droppedBrandingUrls } from "./ui.js";
5
- import { resolveToolkits, type ToolkitConfig } from "./toolkits.js";
8
+ import { droppedBrandingUrls, droppedUiAuthUrls } from "./ui.js";
9
+ import {
10
+ resolveToolkits,
11
+ validateToolkitBindings,
12
+ type Toolkit,
13
+ type ToolkitConfig,
14
+ } from "./toolkits.js";
6
15
  import { memoryStorage } from "./storage/memory.js";
7
16
  import { CONNECTA_VERSION } from "./version.js";
8
17
  import type { ActivityReadGate, ActivityStore } from "./activity.js";
18
+ import type {
19
+ CredentialCheckResult,
20
+ CredentialHealthConfig,
21
+ } from "./credential-health.js";
9
22
  import type {
10
23
  Connector,
11
24
  ConnectaBranding,
@@ -38,9 +51,11 @@ export interface ConnectaConfig {
38
51
  * adding toolkits changes nothing for connections that don't ask for one; an
39
52
  * unknown name is an error, never a silent fallback.
40
53
  *
41
- * Toolkits scope VISIBILITY, not identity: they do not decide *which* team
42
- * member may select which toolkit. Gate that in `auth` (per-member binding is
43
- * a follow-up).
54
+ * Selection is self-service until a toolkit is BOUND to an inbound identity:
55
+ * pass `toolkits` to an auth adapter `bearerToken(secret, { toolkits:
56
+ * ["support"] })` — and that credential may open only those toolkits, and may
57
+ * not connect unscoped unless it also passes `unscoped: true`. An unbound
58
+ * identity keeps the self-service behavior.
44
59
  *
45
60
  * Definitions are validated at construction: an unknown connector id, an
46
61
  * empty connector selection, an empty `includeTools`, a malformed tool
@@ -127,6 +142,19 @@ export interface ConnectaConfig {
127
142
  * real cancellation of the downstream request is a deferred follow-up.
128
143
  */
129
144
  probeTimeoutMs?: number;
145
+ /**
146
+ * Tuning for the proactive credential liveness checks (issue #24) that let a
147
+ * connector's status flip to `auth_required` *before* an agent's call fails.
148
+ * Defaults are safe to leave alone: at most one check per connector per 15
149
+ * minutes, four in flight, 30 s each, triggered opportunistically by inbound
150
+ * authenticated traffic. Only connectors holding a credential connecta stores
151
+ * — an operator-managed `credential`, or a downstream-OAuth grant — are ever
152
+ * checked, and a check never calls a downstream tool.
153
+ *
154
+ * `Connecta.checkCredentials()` is the same check on demand, for a Worker cron
155
+ * trigger or a Node interval.
156
+ */
157
+ credentialHealth?: CredentialHealthConfig;
130
158
  serverInfo?: {
131
159
  name?: string;
132
160
  version?: string;
@@ -152,6 +180,29 @@ export interface Connecta {
152
180
  /** Web-standard fetch handler. Usable as `export default { fetch: connecta.fetch }`. */
153
181
  fetch: (request: Request, env?: unknown, ctx?: unknown) => Promise<Response>;
154
182
  registry: Registry;
183
+ /**
184
+ * Check the stored downstream credentials now — the scheduler-facing half of
185
+ * credential health (issue #24). Wire it to whatever timer the runtime has:
186
+ *
187
+ * ```ts
188
+ * // Cloudflare Workers (wrangler.jsonc: "triggers": { "crons": ["*\/15 * * * *"] })
189
+ * async scheduled(_c, env, ctx) { ctx.waitUntil(build(env).checkCredentials()); }
190
+ * // Node
191
+ * setInterval(() => void connecta.checkCredentials(), 15 * 60_000).unref();
192
+ * ```
193
+ *
194
+ * Returns one outcome per connector considered, including why a connector was
195
+ * skipped (`fresh` is the rate limit: a connector checked less than
196
+ * `credentialHealth.intervalSeconds` ago is not re-checked unless `force`).
197
+ * Never rejects on a connector failure — a broken connector becomes an `error`
198
+ * verdict. Needs a base URL for connector contexts: `publicUrl` supplies it,
199
+ * or pass one.
200
+ */
201
+ checkCredentials: (opts?: {
202
+ baseUrl?: string;
203
+ force?: boolean;
204
+ ids?: string[];
205
+ }) => Promise<CredentialCheckResult[]>;
155
206
  }
156
207
 
157
208
  function defaultLogger(): Logger {
@@ -175,11 +226,13 @@ function normalizeAuth(auth: ConnectaConfig["auth"]): InboundAuth[] {
175
226
  /**
176
227
  * One-time construction warnings for deployment shapes that run fine but are
177
228
  * usually unintended. Warning-only — never throws and never changes behavior;
178
- * each condition emits at most one `logger.warn`. Iterates connectors once.
229
+ * each deployment-wide condition emits at most one `logger.warn`, and each
230
+ * per-connector condition at most one per connector it names.
179
231
  */
180
232
  function warnInsecureConfig(
181
233
  config: ConnectaConfig,
182
234
  inboundAuth: InboundAuth[],
235
+ toolkits: ReadonlyMap<string, Toolkit> | undefined,
183
236
  logger: Logger,
184
237
  ): void {
185
238
  const oauthConnectors = config.connectors.filter((c) => c.finishAuth);
@@ -210,17 +263,62 @@ function warnInsecureConfig(
210
263
  );
211
264
  }
212
265
 
213
- // Toolkits with no inbound auth: a toolkit is a scoped VIEW selected by the
214
- // caller, not an authentication boundary. With nothing gating /mcp, any
215
- // caller picks any toolkitor omits the parameter and sees everything.
216
- if (inboundAuth.length === 0 && config.toolkits) {
217
- logger.warn(
218
- "[connecta] toolkits are configured but there is no inbound " +
219
- "authentication: a toolkit is a scoped view a client selects with " +
220
- "?toolkit=, not an access check, so any caller can choose any toolkit " +
221
- "or omit the parameter and see every connector. Configure `auth` " +
222
- "(for example bearerToken(...) or Clerk).",
223
- );
266
+ // Toolkits that nothing binds to an identity: selection is then self-service,
267
+ // and the boundary organizes the surface rather than protecting it. Three
268
+ // distinct shapes, so three distinct warnings an operator can only act on
269
+ // the one they are actually in.
270
+ //
271
+ // All are keyed off the RESOLVED toolkits, which is the same map `?toolkit=`
272
+ // resolves against, rather than the presence of the config key: `toolkits: {}`
273
+ // is a truthy object that resolves to nothing selectable, so warning about a
274
+ // choice no caller can make would name a risk that does not exist.
275
+ if (toolkits) {
276
+ const unbound = inboundAuth.filter((provider) => !provider.toolkitBinding);
277
+ if (inboundAuth.length === 0) {
278
+ // No auth at all ⇒ no identity exists to bind, so binding is not even the
279
+ // fix here. The open-mode warning above covers the wider exposure.
280
+ logger.warn(
281
+ "[connecta] toolkits are configured but there is no inbound " +
282
+ "authentication: with no identity to bind a toolkit to, any caller " +
283
+ "can choose any toolkit or omit ?toolkit= and see every connector. " +
284
+ "Configure `auth` (for example bearerToken(...) or Clerk), then bind " +
285
+ "each credential with `toolkits: [...]`.",
286
+ );
287
+ } else if (unbound.length === inboundAuth.length) {
288
+ // Authenticated, but every credential may still select every view. This is
289
+ // the shape issue #37 exists to close, and it is invisible without a line
290
+ // saying so: nothing fails, the teams are simply not separated.
291
+ logger.warn(
292
+ "[connecta] toolkits are configured but no inbound identity is bound " +
293
+ "to one: every credential `auth` admits can select any toolkit, or " +
294
+ "omit ?toolkit= and see the whole deployment, so a token handed to " +
295
+ "one team also opens the others' views. Bind each credential with " +
296
+ "`toolkits: [...]` on its auth adapter (add `unscoped: true` for an " +
297
+ "operator credential that should still see everything).",
298
+ );
299
+ } else if (unbound.length > 0) {
300
+ // The dangerous middle: SOME credentials are bound, which is exactly when
301
+ // an operator believes the deployment is separated — while one forgotten
302
+ // provider still opens every view and the whole deployment-wide surface.
303
+ // Naming the unbound providers is the point; an intentionally unrestricted
304
+ // credential says so with `unscoped: true` and stops appearing here.
305
+ const counted = new Map<string, number>();
306
+ for (const provider of unbound) {
307
+ counted.set(provider.kind, (counted.get(provider.kind) ?? 0) + 1);
308
+ }
309
+ const named = [...counted]
310
+ .map(([kind, count]) => (count > 1 ? `${kind} x${count}` : kind))
311
+ .join(", ");
312
+ logger.warn(
313
+ `[connecta] toolkits are bound on some inbound auth providers but not ` +
314
+ `all: ${named} ${unbound.length === 1 ? "declares" : "declare"} no ` +
315
+ "binding, so a caller that provider admits can still select any " +
316
+ "toolkit, connect unscoped, and read the deployment-wide operator " +
317
+ "surfaces — whatever the bound credentials beside it allow. Bind it " +
318
+ "too, or declare the exemption with `toolkits: [...], unscoped: true` " +
319
+ "if it is meant to be an operator credential.",
320
+ );
321
+ }
224
322
  }
225
323
 
226
324
  // Branding URLs that failed their scheme gate. Rendering silently falls back
@@ -235,6 +333,46 @@ function warnInsecureConfig(
235
333
  );
236
334
  }
237
335
 
336
+ // /ui renders exactly one provider's browser sign-in config — the first that
337
+ // offers one, which is the same `find` the /ui route performs — and that
338
+ // provider's URLs reach the browser: frontendApiUrl as the loader's
339
+ // `<script src>`, signInUrl/signUpUrl as the addresses ClerkJS navigates to.
340
+ // Gate-or-drop like a branding href: rendering drops a rejected value and the
341
+ // dashboard then either reports that Clerk could not load or quietly signs in
342
+ // through Clerk's defaults — both confusing symptoms without this line naming
343
+ // the cause. Checking only the rendered provider keeps the claim true — a
344
+ // later provider's uiAuth never reaches the page, so there is nothing there
345
+ // to warn about.
346
+ const uiAuthProvider = inboundAuth.find((provider) => provider.uiAuth);
347
+ const droppedUiAuth = droppedUiAuthUrls(uiAuthProvider?.uiAuth);
348
+ if (uiAuthProvider && droppedUiAuth.length > 0) {
349
+ logger.warn(
350
+ `[connecta] inbound auth provider "${uiAuthProvider.kind}" had ` +
351
+ `${droppedUiAuth.join(", ")} dropped: every uiAuth URL reaches the ` +
352
+ "browser — as the sign-in loader's source, or as a place Clerk sends " +
353
+ "the operator — so each must be an absolute https URL. A dropped " +
354
+ "value reaches no part of the page: without frontendApiUrl /ui renders " +
355
+ "no loader and cannot start a sign-in, and without signInUrl/signUpUrl " +
356
+ "it signs in through Clerk's defaults.",
357
+ );
358
+ }
359
+
360
+ // A credential test hook that cannot test the declared credential shape.
361
+ // The shape picks the hook (see `credentialTestRule`) and the other one is
362
+ // never substituted, so the connector is simply not testable: /ui offers no
363
+ // Test action and the route answers 400. Without this line the only way to
364
+ // discover the mistake is to click a button that isn't there.
365
+ for (const connector of config.connectors) {
366
+ const { mismatch } = credentialTestRule(connector);
367
+ if (!mismatch) continue;
368
+ logger.warn(
369
+ `[connecta] connector "${connector.id}" cannot test its credential: ` +
370
+ `${describeCredentialTestMismatch(mismatch)}. /ui offers no Test ` +
371
+ `action and POST /ui/credentials/${connector.id}/test answers 400 ` +
372
+ "until the matching hook is implemented.",
373
+ );
374
+ }
375
+
238
376
  // OAuth connectors whose callback performs no state/CSRF check: the public
239
377
  // /oauth/callback/<id> route would exchange any delivered code.
240
378
  for (const connector of oauthConnectors) {
@@ -269,6 +407,7 @@ export function createConnecta(config: ConnectaConfig): Connecta {
269
407
  persistToolCatalog: config.persistToolCatalog,
270
408
  toolCatalogStaleSeconds: config.toolCatalogStaleSeconds,
271
409
  maxResultBytes: config.maxResultBytes,
410
+ credentialHealth: config.credentialHealth,
272
411
  });
273
412
  // Throws on every structural mistake it can see (see resolveToolkits): a
274
413
  // typo must not become a scope the operator never wrote. Note this is about
@@ -276,7 +415,11 @@ export function createConnecta(config: ConnectaConfig): Connecta {
276
415
  // scopes visibility, and `auth` remains the thing deciding who gets in.
277
416
  const toolkits = resolveToolkits(config.toolkits, config.connectors);
278
417
  const inboundAuth = normalizeAuth(config.auth);
279
- warnInsecureConfig(config, inboundAuth, logger);
418
+ // Same contract for the identity half: a binding that names a toolkit this
419
+ // deployment does not declare would deny that credential every connection,
420
+ // with a 403 its client reports as a transport failure. Throw here instead.
421
+ validateToolkitBindings(inboundAuth, toolkits);
422
+ warnInsecureConfig(config, inboundAuth, toolkits, logger);
280
423
  const handler = createFetchHandler({
281
424
  registry,
282
425
  auth: inboundAuth,
@@ -307,6 +450,29 @@ export function createConnecta(config: ConnectaConfig): Connecta {
307
450
  : undefined,
308
451
  ),
309
452
  registry,
453
+ checkCredentials: (opts = {}) => {
454
+ // A scheduled check has no inbound request to derive an origin from, and
455
+ // a connector context without one would mint OAuth redirect URIs against
456
+ // a guess. Say so instead: the fix is one config line.
457
+ const baseUrl = opts.baseUrl ?? config.publicUrl;
458
+ if (!baseUrl) {
459
+ // Rejected, not thrown: the callers this is written for are
460
+ // `ctx.waitUntil(...)` and `.catch(...)` on the returned promise, and a
461
+ // synchronous throw escapes both — it would take down a scheduled
462
+ // handler instead of being reported by it.
463
+ return Promise.reject(
464
+ new Error(
465
+ "checkCredentials() needs a base URL: set `publicUrl` on the " +
466
+ "config (recommended — it is also what downstream OAuth " +
467
+ "callbacks use) or pass checkCredentials({ baseUrl }).",
468
+ ),
469
+ );
470
+ }
471
+ return registry.checkCredentialHealth(baseUrl, {
472
+ ...(opts.force !== undefined ? { force: opts.force } : {}),
473
+ ...(opts.ids ? { ids: opts.ids } : {}),
474
+ });
475
+ },
310
476
  };
311
477
  }
312
478
 
@@ -320,15 +486,30 @@ export type { ConnectorCallErrorCode, CallErrorDetails } from "./errors.js";
320
486
  export { validateToolInput } from "./validate.js";
321
487
  export type { ValidateToolInputOptions } from "./validate.js";
322
488
  export { bearerToken } from "./auth/bearer.js";
489
+ export type { BearerTokenOptions } from "./auth/bearer.js";
323
490
  export { memoryStorage } from "./storage/memory.js";
324
491
  export { CONNECTA_VERSION } from "./version.js";
325
492
  // Registry is reachable through `Connecta.registry`, so its type is public;
326
493
  // the class itself, the credential vault, and the meta-tool/sandbox factories
327
494
  // are internal factoring and are deliberately not part of the API surface.
328
495
  export type { Registry } from "./registry.js";
329
- // Config-as-code shapes for `ConnectaConfig.toolkits`. The resolved `Toolkit`
330
- // and the `ScopedRegistry` that enforces it are internal factoring.
331
- export type { ToolkitConfig, ToolkitDefinition } from "./toolkits.js";
496
+ // Config-as-code shapes for `ConnectaConfig.toolkits` and the identity bindings
497
+ // that gate them. The resolved `Toolkit` and the `ScopedRegistry` that enforces
498
+ // it are internal factoring.
499
+ export type {
500
+ ToolkitBindingOptions,
501
+ ToolkitConfig,
502
+ ToolkitDefinition,
503
+ } from "./toolkits.js";
504
+ // Credential health: the config shape, and the result shape a scheduled
505
+ // `checkCredentials()` returns. The checker itself is internal factoring.
506
+ export type {
507
+ CredentialCheckResult,
508
+ CredentialCheckSkip,
509
+ CredentialCheckState,
510
+ CredentialHealthConfig,
511
+ CredentialHealthRecord,
512
+ } from "./credential-health.js";
332
513
 
333
514
  export type { RemoteMcpOptions, RemoteMcpAuth } from "./connectors/remote-mcp.js";
334
515
  export type { ApiOptions, ApiTool } from "./connectors/api.js";
@@ -346,6 +527,7 @@ export type {
346
527
  Executor,
347
528
  ExecutorProvider,
348
529
  InboundAuth,
530
+ ToolkitBinding,
349
531
  UiAuthConfig,
350
532
  AuthResult,
351
533
  JsonSchema,