@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/index.ts CHANGED
@@ -19,6 +19,7 @@ import type {
19
19
  CredentialCheckResult,
20
20
  CredentialHealthConfig,
21
21
  } from "./credential-health.js";
22
+ import type { DeferredWork } from "./connector-scope.js";
22
23
  import type {
23
24
  Connector,
24
25
  ConnectaBranding,
@@ -82,7 +83,8 @@ export interface ConnectaDiscoveryConfig {
82
83
  * Deadline (ms) for each downstream probe/catalog call fanned out by
83
84
  * `list_connectors`, `search_tools`, and `describe_tools`. Defaults to
84
85
  * 30_000. A timed-out connector degrades independently; this does not apply
85
- * to tool calls or currently abort the underlying fetch.
86
+ * to tool calls. Catalog walks receive the same cancellation signal, which
87
+ * aborts an in-flight page where supported and prevents another from starting.
86
88
  */
87
89
  probeTimeoutMs?: number;
88
90
  }
@@ -191,7 +193,10 @@ export interface Connecta {
191
193
  *
192
194
  * ```ts
193
195
  * // Cloudflare Workers (wrangler.jsonc: "triggers": { "crons": ["*\/15 * * * *"] })
194
- * async scheduled(_c, env, ctx) { ctx.waitUntil(build(env).checkCredentials()); }
196
+ * async scheduled(_c, env, ctx) {
197
+ * const defer = ctx.waitUntil.bind(ctx);
198
+ * ctx.waitUntil(build(env).checkCredentials({ defer }));
199
+ * }
195
200
  * // Node
196
201
  * setInterval(() => void connecta.checkCredentials(), 15 * 60_000).unref();
197
202
  * ```
@@ -207,7 +212,14 @@ export interface Connecta {
207
212
  baseUrl?: string;
208
213
  force?: boolean;
209
214
  ids?: string[];
215
+ /**
216
+ * Runtime continuation for bounded teardown after the check result is ready.
217
+ * Workers should pass `ctx.waitUntil.bind(ctx)`.
218
+ */
219
+ defer?: DeferredWork;
210
220
  }) => Promise<CredentialCheckResult[]>;
221
+ /** Drain and release configured executor resources. Idempotent. */
222
+ close: () => Promise<void>;
211
223
  }
212
224
 
213
225
  function defaultLogger(): Logger {
@@ -504,6 +516,7 @@ export function createConnecta(config: ConnectaConfig): Connecta {
504
516
  branding: config.branding,
505
517
  ...(toolkits ? { toolkits } : {}),
506
518
  });
519
+ let closePromise: Promise<void> | undefined;
507
520
  return {
508
521
  fetch: (request, _env, ctx) =>
509
522
  handler(
@@ -531,10 +544,20 @@ export function createConnecta(config: ConnectaConfig): Connecta {
531
544
  ),
532
545
  );
533
546
  }
534
- return registry.checkCredentialHealth(baseUrl, {
535
- ...(opts.force !== undefined ? { force: opts.force } : {}),
536
- ...(opts.ids ? { ids: opts.ids } : {}),
547
+ return registry.checkCredentialHealth(
548
+ baseUrl,
549
+ {
550
+ ...(opts.force !== undefined ? { force: opts.force } : {}),
551
+ ...(opts.ids ? { ids: opts.ids } : {}),
552
+ },
553
+ opts.defer,
554
+ );
555
+ },
556
+ close: async () => {
557
+ closePromise ??= Promise.resolve().then(async () => {
558
+ await config.executor?.close?.();
537
559
  });
560
+ await closePromise;
538
561
  },
539
562
  };
540
563
  }
@@ -590,8 +613,10 @@ export type {
590
613
  ConnectorContext,
591
614
  ConnectorStatus,
592
615
  CredentialTestResult,
616
+ AdmittingExecutor,
593
617
  ExecuteResult,
594
618
  Executor,
619
+ ExecutorLease,
595
620
  ExecutorProvider,
596
621
  InboundAuth,
597
622
  ToolkitBinding,
package/src/meta-tools.ts CHANGED
@@ -6,7 +6,10 @@ import {
6
6
  type ActivityCallSource,
7
7
  type ActivityRequestContext,
8
8
  } from "./activity.js";
9
- import { closeConnectorScope } from "./connector-scope.js";
9
+ import {
10
+ closeConnectorScope,
11
+ type DeferredWork,
12
+ } from "./connector-scope.js";
10
13
  import { unwrapMcpResult } from "./mcp-result.js";
11
14
  import {
12
15
  classifyCallError,
@@ -30,7 +33,7 @@ import {
30
33
  import {
31
34
  DEFAULT_PROBE_TIMEOUT_MS,
32
35
  normalizeTimeoutMs,
33
- withTimeout,
36
+ withAbortableTimeout,
34
37
  } from "./timeout.js";
35
38
  import { credentialVerdictApplies } from "./credential-health.js";
36
39
  import type { ConnectorStatus, KVStorage, ToolDef } from "./types.js";
@@ -586,6 +589,8 @@ export function createMetaTools(
586
589
  /** Per-connector deadline for the list/search/describe probe fan-out. Default 30_000. */
587
590
  probeTimeoutMs?: number;
588
591
  activity?: ActivityRequestContext;
592
+ /** Runtime continuation for the bounded tail of probe-owned teardown. */
593
+ defer?: DeferredWork;
589
594
  } = {},
590
595
  ) {
591
596
  // Already normalized and warned about at registry construction.
@@ -597,6 +602,18 @@ export function createMetaTools(
597
602
  // identity lets remote connectors reuse one downstream client inside that
598
603
  // request without leaking request-bound I/O into the next one.
599
604
  const requestScope = {};
605
+ const withProbeDeadline = <T>(
606
+ label: string,
607
+ operation: (options: {
608
+ signal: AbortSignal;
609
+ timeoutMs: number;
610
+ }) => Promise<T>,
611
+ ) =>
612
+ withAbortableTimeout(
613
+ (signal) => operation({ signal, timeoutMs: probeTimeoutMs }),
614
+ probeTimeoutMs,
615
+ label,
616
+ );
600
617
 
601
618
  interface RunCallOutcome {
602
619
  toolResult: ToolResult;
@@ -907,8 +924,8 @@ export function createMetaTools(
907
924
  // call scope. Closing it cannot defeat call_tool/batch/execute_code reuse.
908
925
  const connectors = registry.listConnectors();
909
926
  const scope = probe ? {} : requestScope;
910
- const out = await Promise.all(
911
- connectors.map(async (c) => {
927
+ const pending = connectors.map(
928
+ async (c) => {
912
929
  const statusStarted = Date.now();
913
930
  const observed = registry.healthFor(c.id);
914
931
  const verdict = await registry.credentialHealthFor(c.id);
@@ -917,10 +934,10 @@ export function createMetaTools(
917
934
  | { state: "ok" | "error" | "unknown"; message?: string };
918
935
  if (probe) {
919
936
  try {
920
- status = await withTimeout(
921
- registry.statusFor(c.id, baseUrl, scope),
922
- probeTimeoutMs,
937
+ status = await withProbeDeadline(
923
938
  `list_connectors probe of "${c.id}"`,
939
+ (options) =>
940
+ registry.statusFor(c.id, baseUrl, scope, options),
924
941
  );
925
942
  } catch (err) {
926
943
  // A probe that outran probeTimeoutMs (or otherwise threw)
@@ -1004,14 +1021,45 @@ export function createMetaTools(
1004
1021
  // the first (now stale) authorization URL.
1005
1022
  if (probe && status.state === "ok") {
1006
1023
  try {
1007
- tools = await withTimeout(
1008
- registry.refreshTools(c.id, baseUrl, scope),
1009
- probeTimeoutMs,
1024
+ tools = await withProbeDeadline(
1010
1025
  `list_connectors catalog refresh of "${c.id}"`,
1026
+ (options) =>
1027
+ registry.refreshTools(c.id, baseUrl, scope, options),
1011
1028
  );
1012
1029
  registry.recordSuccess(c.id, Date.now() - statusStarted);
1013
1030
  } catch (err) {
1014
- status = { state: "error" as const, message: msg(err) };
1031
+ const details = classifyCallError(err);
1032
+ if (details.code === "auth_required") {
1033
+ let authStatus: ConnectorStatus | undefined;
1034
+ try {
1035
+ authStatus = await withProbeDeadline(
1036
+ `list_connectors authorization status of "${c.id}"`,
1037
+ (options) =>
1038
+ registry.statusFor(c.id, baseUrl, scope, options),
1039
+ );
1040
+ } catch {
1041
+ // The typed auth verdict is still authoritative; this second
1042
+ // read exists only to recover the connector's pending URL.
1043
+ }
1044
+ status =
1045
+ authStatus?.state === "auth_required"
1046
+ ? authStatus
1047
+ : {
1048
+ state: "auth_required" as const,
1049
+ message: details.message,
1050
+ };
1051
+ await registry.recordCredentialHealth(c.id, {
1052
+ state: "auth_required",
1053
+ checkedAt,
1054
+ ...(status.message ? { message: status.message } : {}),
1055
+ ...("authorizationUrl" in status &&
1056
+ status.authorizationUrl
1057
+ ? { authorizationUrl: status.authorizationUrl }
1058
+ : {}),
1059
+ });
1060
+ } else {
1061
+ status = { state: "error" as const, message: msg(err) };
1062
+ }
1015
1063
  registry.recordFailure(c.id, Date.now() - statusStarted, err);
1016
1064
  }
1017
1065
  }
@@ -1036,17 +1084,24 @@ export function createMetaTools(
1036
1084
  : {}),
1037
1085
  ...(status.message ? { message: status.message } : {}),
1038
1086
  };
1039
- }),
1040
- ).finally(async () => {
1041
- if (!probe) return;
1042
- await Promise.all(
1043
- connectors.map((connector) =>
1044
- closeConnectorScope(
1045
- connector,
1046
- registry.contextFor(connector.id, baseUrl, scope),
1047
- ),
1087
+ },
1088
+ );
1089
+ if (!probe) {
1090
+ return jsonResult({ connectors: await Promise.all(pending) });
1091
+ }
1092
+ const settled = await Promise.allSettled(pending);
1093
+ await Promise.all(
1094
+ connectors.map((connector) =>
1095
+ closeConnectorScope(
1096
+ connector,
1097
+ registry.contextFor(connector.id, baseUrl, scope),
1098
+ opts.defer,
1048
1099
  ),
1049
- );
1100
+ ),
1101
+ );
1102
+ const out = settled.map((result) => {
1103
+ if (result.status === "rejected") throw result.reason;
1104
+ return result.value;
1050
1105
  });
1051
1106
  return jsonResult({ connectors: out });
1052
1107
  },
@@ -1079,10 +1134,10 @@ export function createMetaTools(
1079
1134
  }> = [];
1080
1135
  const catalogs = await Promise.allSettled(
1081
1136
  conns.map((c) =>
1082
- withTimeout(
1083
- registry.getTools(c.id, baseUrl, requestScope),
1084
- probeTimeoutMs,
1137
+ withProbeDeadline(
1085
1138
  `search_tools probe of "${c.id}"`,
1139
+ (options) =>
1140
+ registry.getTools(c.id, baseUrl, requestScope, options),
1086
1141
  ),
1087
1142
  ),
1088
1143
  );
@@ -1208,10 +1263,10 @@ export function createMetaTools(
1208
1263
  ];
1209
1264
  const loaded = await Promise.allSettled(
1210
1265
  connectorIds.map((id) =>
1211
- withTimeout(
1212
- registry.getTools(id, baseUrl, requestScope),
1213
- probeTimeoutMs,
1266
+ withProbeDeadline(
1214
1267
  `describe_tools probe of "${id}"`,
1268
+ (options) =>
1269
+ registry.getTools(id, baseUrl, requestScope, options),
1215
1270
  ),
1216
1271
  ),
1217
1272
  );
@@ -1538,12 +1593,14 @@ export function registerMetaTools(
1538
1593
  defaultToolTimeoutMs?: number;
1539
1594
  probeTimeoutMs?: number;
1540
1595
  activity?: ActivityRequestContext;
1596
+ defer?: DeferredWork;
1541
1597
  },
1542
1598
  ): void {
1543
1599
  const mt = createMetaTools(registry, ctx.baseUrl, {
1544
1600
  defaultToolTimeoutMs: ctx.defaultToolTimeoutMs,
1545
1601
  probeTimeoutMs: ctx.probeTimeoutMs,
1546
1602
  activity: ctx.activity,
1603
+ defer: ctx.defer,
1547
1604
  });
1548
1605
 
1549
1606
  server.registerTool(
package/src/node.ts CHANGED
@@ -32,6 +32,7 @@ const BODY_TOO_LARGE = Symbol("body-too-large");
32
32
  async function toRequest(
33
33
  req: IncomingMessage,
34
34
  maxBodyBytes: number,
35
+ signal: AbortSignal,
35
36
  ): Promise<Request | typeof BODY_TOO_LARGE> {
36
37
  const host = req.headers.host ?? "localhost";
37
38
  const proto =
@@ -60,6 +61,7 @@ async function toRequest(
60
61
  method,
61
62
  headers,
62
63
  body,
64
+ signal,
63
65
  // Required by Node's fetch when sending a body stream.
64
66
  ...(body ? { duplex: "half" } : {}),
65
67
  } as RequestInit);
@@ -111,8 +113,27 @@ export function listen(
111
113
 
112
114
  const server = createServer((req, res) => {
113
115
  void (async () => {
116
+ const controller = new AbortController();
117
+ const abortRequest = () => {
118
+ if (!controller.signal.aborted) {
119
+ controller.abort(new Error("HTTP client disconnected."));
120
+ }
121
+ };
122
+ const abortIncompleteRequest = () => {
123
+ if (!req.complete) abortRequest();
124
+ };
125
+ const abortIncompleteResponse = () => {
126
+ if (!res.writableFinished) abortRequest();
127
+ };
128
+ req.once("aborted", abortRequest);
129
+ req.once("close", abortIncompleteRequest);
130
+ res.once("close", abortIncompleteResponse);
114
131
  try {
115
- const request = await toRequest(req, maxBodyBytes);
132
+ const request = await toRequest(
133
+ req,
134
+ maxBodyBytes,
135
+ controller.signal,
136
+ );
116
137
  if (request === BODY_TOO_LARGE) {
117
138
  res.statusCode = 413;
118
139
  // The request stream was abandoned mid-body, so this connection
@@ -125,6 +146,12 @@ export function listen(
125
146
  const response = await connecta.fetch(request, undefined, ctx);
126
147
  await writeResponse(res, response);
127
148
  } catch (error) {
149
+ // A client that deliberately went away can make either the request
150
+ // handler or response pipeline reject. The request's abort signal is
151
+ // the authoritative classification: there is no caller left to answer,
152
+ // and logging an expected disconnect would turn cancellation into an
153
+ // error-log spam vector.
154
+ if (controller.signal.aborted) return;
128
155
  // A headless deployment has nothing else to go on; never swallow this.
129
156
  console.error("[connecta] request failed", error);
130
157
  if (res.headersSent) {
@@ -136,6 +163,10 @@ export function listen(
136
163
  }
137
164
  res.statusCode = 500;
138
165
  res.end("Internal Server Error");
166
+ } finally {
167
+ req.removeListener("aborted", abortRequest);
168
+ req.removeListener("close", abortIncompleteRequest);
169
+ res.removeListener("close", abortIncompleteResponse);
139
170
  }
140
171
  })();
141
172
  });
@@ -152,11 +183,14 @@ export function listen(
152
183
  process.exit(1);
153
184
  }, timeoutMs);
154
185
  forced.unref?.();
186
+ // Stop queued/running sandbox work now, not after server.close has
187
+ // waited for the very requests those children are holding open.
188
+ const executorClose = connecta.close();
155
189
  // close() alone waits on idle keep-alive sockets, so a single browser
156
190
  // tab would stall every shutdown until the force-exit deadline.
157
191
  server.closeIdleConnections();
158
192
  server.close(() => {
159
- void Promise.allSettled([...pending]).then(() => {
193
+ void Promise.allSettled([...pending, executorClose]).then(() => {
160
194
  clearTimeout(forced);
161
195
  process.exit(0);
162
196
  });
package/src/registry.ts CHANGED
@@ -6,7 +6,11 @@ import type {
6
6
  Logger,
7
7
  ToolDef,
8
8
  } from "./types.js";
9
- import type { CredentialVault } from "./credentials.js";
9
+ import {
10
+ storedCredentialShape,
11
+ type CredentialVault,
12
+ } from "./credentials.js";
13
+ import { ConnectorCallError } from "./errors.js";
10
14
  import {
11
15
  CredentialHealthChecker,
12
16
  type CredentialCheckOptions,
@@ -14,6 +18,7 @@ import {
14
18
  type CredentialHealthConfig,
15
19
  type CredentialHealthRecord,
16
20
  } from "./credential-health.js";
21
+ import type { DeferredWork } from "./connector-scope.js";
17
22
  import { splitAddress, type Toolkit } from "./toolkits.js";
18
23
 
19
24
  const ID_RE = /^[a-z0-9_-]+$/;
@@ -154,6 +159,11 @@ function msg(err: unknown): string {
154
159
  return err instanceof Error ? err.message : String(err);
155
160
  }
156
161
 
162
+ type ConnectorOperationOptions = Pick<
163
+ ConnectorContext,
164
+ "signal" | "timeoutMs"
165
+ >;
166
+
157
167
  /**
158
168
  * The registry surface a per-connection MCP server consumes: every meta-tool
159
169
  * (`src/meta-tools.ts`) and the `execute_code` sandbox bridge (`src/execute.ts`)
@@ -178,18 +188,20 @@ export interface RegistryView {
178
188
  id: string,
179
189
  baseUrl: string,
180
190
  requestScope?: object,
191
+ callOptions?: ConnectorOperationOptions,
181
192
  ): Promise<ToolDef[]>;
182
193
  refreshTools(
183
194
  id: string,
184
195
  baseUrl: string,
185
196
  requestScope?: object,
197
+ callOptions?: ConnectorOperationOptions,
186
198
  ): Promise<ToolDef[]>;
187
199
  peekTools(id: string): ToolDef[] | undefined;
188
200
  contextFor(
189
201
  id: string,
190
202
  baseUrl: string,
191
203
  requestScope?: object,
192
- callOptions?: { signal?: AbortSignal; timeoutMs?: number },
204
+ callOptions?: ConnectorOperationOptions,
193
205
  ): ConnectorContext;
194
206
  resultsStorage(): KVStorage;
195
207
  recordSuccess(id: string, latencyMs: number): void;
@@ -209,6 +221,7 @@ export interface RegistryView {
209
221
  id: string,
210
222
  baseUrl: string,
211
223
  requestScope?: object,
224
+ callOptions?: ConnectorOperationOptions,
212
225
  ): Promise<ConnectorStatus>;
213
226
  invalidateStored(id: string): Promise<void>;
214
227
  }
@@ -352,21 +365,31 @@ export class Registry implements RegistryView {
352
365
  id: string,
353
366
  baseUrl: string,
354
367
  requestScope: object = {},
355
- callOptions: { signal?: AbortSignal; timeoutMs?: number } = {},
368
+ callOptions: ConnectorOperationOptions = {},
356
369
  ): ConnectorContext {
370
+ const credentialConfig = this.connectors.get(id)?.credential;
371
+ let credentialAccess: ConnectorContext["credential"];
372
+ if (this.opts.credentialVault && credentialConfig) {
373
+ const vault = this.opts.credentialVault;
374
+ const readValues = async () => {
375
+ const values = await vault.getAll(id);
376
+ const shape = storedCredentialShape(credentialConfig, values);
377
+ if (shape.state === "mismatch") {
378
+ throw new ConnectorCallError("auth_required", shape.message);
379
+ }
380
+ return values;
381
+ };
382
+ credentialAccess = {
383
+ get: async (field = "value") =>
384
+ (await readValues())?.[field] ?? null,
385
+ getAll: readValues,
386
+ };
387
+ }
357
388
  return {
358
389
  storage: namespaced(this.opts.storage, `conn:${id}:`),
359
390
  logger: this.opts.logger,
360
391
  baseUrl,
361
- ...(this.opts.credentialVault && this.connectors.get(id)?.credential
362
- ? {
363
- credential: {
364
- get: (field?: string) =>
365
- this.opts.credentialVault!.get(id, field),
366
- getAll: () => this.opts.credentialVault!.getAll(id),
367
- },
368
- }
369
- : {}),
392
+ ...(credentialAccess ? { credential: credentialAccess } : {}),
370
393
  requestScope,
371
394
  ...callOptions,
372
395
  };
@@ -441,13 +464,14 @@ export class Registry implements RegistryView {
441
464
  id: string,
442
465
  baseUrl: string,
443
466
  requestScope?: object,
467
+ callOptions: ConnectorOperationOptions = {},
444
468
  ): Promise<ToolDef[]> {
445
469
  const connector = this.connectors.get(id);
446
470
  if (!connector) throw new Error(`Unknown connector "${id}"`);
447
471
  const tools = connector.staticTools
448
472
  ? connector.staticTools
449
473
  : await connector.listTools(
450
- this.contextFor(id, baseUrl, requestScope),
474
+ this.contextFor(id, baseUrl, requestScope, callOptions),
451
475
  );
452
476
  const now = Date.now();
453
477
  const previous = this.cache.get(id);
@@ -479,6 +503,7 @@ export class Registry implements RegistryView {
479
503
  id: string,
480
504
  baseUrl: string,
481
505
  requestScope?: object,
506
+ callOptions: ConnectorOperationOptions = {},
482
507
  ): Promise<ToolDef[]> {
483
508
  const connector = this.connectors.get(id);
484
509
  if (!connector) throw new Error(`Unknown connector "${id}"`);
@@ -512,7 +537,7 @@ export class Registry implements RegistryView {
512
537
  }
513
538
 
514
539
  try {
515
- return await this.refreshTools(id, baseUrl, requestScope);
540
+ return await this.refreshTools(id, baseUrl, requestScope, callOptions);
516
541
  } catch (err) {
517
542
  if (stale) {
518
543
  this.opts.logger.warn(
@@ -594,8 +619,9 @@ export class Registry implements RegistryView {
594
619
  checkCredentialHealth(
595
620
  baseUrl: string,
596
621
  opts?: CredentialCheckOptions,
622
+ defer?: DeferredWork,
597
623
  ): Promise<CredentialCheckResult[]> {
598
- return this.credentialHealth.check(baseUrl, opts);
624
+ return this.credentialHealth.check(baseUrl, opts, defer);
599
625
  }
600
626
 
601
627
  /**
@@ -606,8 +632,9 @@ export class Registry implements RegistryView {
606
632
  */
607
633
  sweepCredentialHealthIfDue(
608
634
  baseUrl: string,
635
+ defer?: DeferredWork,
609
636
  ): Promise<CredentialCheckResult[]> | undefined {
610
- return this.credentialHealth.sweepIfDue(baseUrl);
637
+ return this.credentialHealth.sweepIfDue(baseUrl, defer);
611
638
  }
612
639
 
613
640
  /**
@@ -626,10 +653,11 @@ export class Registry implements RegistryView {
626
653
  id: string,
627
654
  baseUrl: string,
628
655
  requestScope: object = {},
656
+ callOptions: ConnectorOperationOptions = {},
629
657
  ): Promise<ConnectorStatus> {
630
658
  const connector = this.connectors.get(id);
631
659
  if (!connector) return { state: "error", message: "Unknown connector" };
632
- const ctx = this.contextFor(id, baseUrl, requestScope);
660
+ const ctx = this.contextFor(id, baseUrl, requestScope, callOptions);
633
661
  if (connector.status) {
634
662
  try {
635
663
  return await connector.status(ctx);
@@ -638,7 +666,7 @@ export class Registry implements RegistryView {
638
666
  }
639
667
  }
640
668
  try {
641
- await this.getTools(id, baseUrl, requestScope);
669
+ await this.getTools(id, baseUrl, requestScope, callOptions);
642
670
  return { state: "ok" };
643
671
  } catch (err) {
644
672
  return { state: "error", message: msg(err) };
@@ -782,11 +810,12 @@ export class ScopedRegistry implements RegistryView {
782
810
  id: string,
783
811
  baseUrl: string,
784
812
  requestScope?: object,
813
+ callOptions: ConnectorOperationOptions = {},
785
814
  ): Promise<ToolDef[]> {
786
815
  if (!this.visible(id)) throw this.unknownConnector(id);
787
816
  return this.inScopeTools(
788
817
  id,
789
- await this.base.getTools(id, baseUrl, requestScope),
818
+ await this.base.getTools(id, baseUrl, requestScope, callOptions),
790
819
  );
791
820
  }
792
821
 
@@ -794,11 +823,12 @@ export class ScopedRegistry implements RegistryView {
794
823
  id: string,
795
824
  baseUrl: string,
796
825
  requestScope?: object,
826
+ callOptions: ConnectorOperationOptions = {},
797
827
  ): Promise<ToolDef[]> {
798
828
  if (!this.visible(id)) throw this.unknownConnector(id);
799
829
  return this.inScopeTools(
800
830
  id,
801
- await this.base.refreshTools(id, baseUrl, requestScope),
831
+ await this.base.refreshTools(id, baseUrl, requestScope, callOptions),
802
832
  );
803
833
  }
804
834
 
@@ -812,7 +842,7 @@ export class ScopedRegistry implements RegistryView {
812
842
  id: string,
813
843
  baseUrl: string,
814
844
  requestScope: object = {},
815
- callOptions: { signal?: AbortSignal; timeoutMs?: number } = {},
845
+ callOptions: ConnectorOperationOptions = {},
816
846
  ): ConnectorContext {
817
847
  // Unreachable through the meta-tools (they resolve first), so a throw here
818
848
  // is a loud backstop rather than a silent grant of connector storage and
@@ -897,12 +927,13 @@ export class ScopedRegistry implements RegistryView {
897
927
  id: string,
898
928
  baseUrl: string,
899
929
  requestScope: object = {},
930
+ callOptions: { signal?: AbortSignal; timeoutMs?: number } = {},
900
931
  ): Promise<ConnectorStatus> {
901
932
  // Same shape the unscoped registry returns for an unregistered id.
902
933
  if (!this.visible(id)) {
903
934
  return { state: "error", message: "Unknown connector" };
904
935
  }
905
- return this.base.statusFor(id, baseUrl, requestScope);
936
+ return this.base.statusFor(id, baseUrl, requestScope, callOptions);
906
937
  }
907
938
 
908
939
  async invalidateStored(id: string): Promise<void> {