@zackbart/connecta 0.7.3 → 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 (86) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +3 -1
  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 +47 -16
  9. package/dist/connectors/remote-mcp.js.map +1 -1
  10. package/dist/credential-health.d.ts +3 -2
  11. package/dist/credential-health.d.ts.map +1 -1
  12. package/dist/credential-health.js +9 -9
  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 +459 -311
  41. package/dist/executors/quickjs.js.map +1 -1
  42. package/dist/index.d.ts +13 -2
  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 +3 -2
  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 +3 -2
  54. package/dist/registry.d.ts.map +1 -1
  55. package/dist/registry.js +4 -4
  56. package/dist/registry.js.map +1 -1
  57. package/dist/server.d.ts.map +1 -1
  58. package/dist/server.js +11 -4
  59. package/dist/server.js.map +1 -1
  60. package/dist/types.d.ts +17 -0
  61. package/dist/types.d.ts.map +1 -1
  62. package/dist/ui.d.ts +2 -1
  63. package/dist/ui.d.ts.map +1 -1
  64. package/dist/ui.js +3 -3
  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 +1 -1
  69. package/src/connector-scope.ts +43 -18
  70. package/src/connectors/remote-mcp.ts +56 -19
  71. package/src/credential-health.ts +20 -6
  72. package/src/execute.ts +93 -35
  73. package/src/executor-admission.ts +229 -0
  74. package/src/executor-result.ts +63 -0
  75. package/src/executors/quickjs-child.ts +168 -0
  76. package/src/executors/quickjs-protocol.ts +77 -0
  77. package/src/executors/quickjs-runtime.ts +440 -0
  78. package/src/executors/quickjs.ts +660 -353
  79. package/src/index.ts +28 -4
  80. package/src/meta-tools.ts +9 -1
  81. package/src/node.ts +36 -2
  82. package/src/registry.ts +5 -2
  83. package/src/server.ts +10 -2
  84. package/src/types.ts +17 -0
  85. package/src/ui.ts +6 -1
  86. 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,
@@ -192,7 +193,10 @@ export interface Connecta {
192
193
  *
193
194
  * ```ts
194
195
  * // Cloudflare Workers (wrangler.jsonc: "triggers": { "crons": ["*\/15 * * * *"] })
195
- * 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
+ * }
196
200
  * // Node
197
201
  * setInterval(() => void connecta.checkCredentials(), 15 * 60_000).unref();
198
202
  * ```
@@ -208,7 +212,14 @@ export interface Connecta {
208
212
  baseUrl?: string;
209
213
  force?: boolean;
210
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;
211
220
  }) => Promise<CredentialCheckResult[]>;
221
+ /** Drain and release configured executor resources. Idempotent. */
222
+ close: () => Promise<void>;
212
223
  }
213
224
 
214
225
  function defaultLogger(): Logger {
@@ -505,6 +516,7 @@ export function createConnecta(config: ConnectaConfig): Connecta {
505
516
  branding: config.branding,
506
517
  ...(toolkits ? { toolkits } : {}),
507
518
  });
519
+ let closePromise: Promise<void> | undefined;
508
520
  return {
509
521
  fetch: (request, _env, ctx) =>
510
522
  handler(
@@ -532,10 +544,20 @@ export function createConnecta(config: ConnectaConfig): Connecta {
532
544
  ),
533
545
  );
534
546
  }
535
- return registry.checkCredentialHealth(baseUrl, {
536
- ...(opts.force !== undefined ? { force: opts.force } : {}),
537
- ...(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?.();
538
559
  });
560
+ await closePromise;
539
561
  },
540
562
  };
541
563
  }
@@ -591,8 +613,10 @@ export type {
591
613
  ConnectorContext,
592
614
  ConnectorStatus,
593
615
  CredentialTestResult,
616
+ AdmittingExecutor,
594
617
  ExecuteResult,
595
618
  Executor,
619
+ ExecutorLease,
596
620
  ExecutorProvider,
597
621
  InboundAuth,
598
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,
@@ -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.
@@ -1090,6 +1095,7 @@ export function createMetaTools(
1090
1095
  closeConnectorScope(
1091
1096
  connector,
1092
1097
  registry.contextFor(connector.id, baseUrl, scope),
1098
+ opts.defer,
1093
1099
  ),
1094
1100
  ),
1095
1101
  );
@@ -1587,12 +1593,14 @@ export function registerMetaTools(
1587
1593
  defaultToolTimeoutMs?: number;
1588
1594
  probeTimeoutMs?: number;
1589
1595
  activity?: ActivityRequestContext;
1596
+ defer?: DeferredWork;
1590
1597
  },
1591
1598
  ): void {
1592
1599
  const mt = createMetaTools(registry, ctx.baseUrl, {
1593
1600
  defaultToolTimeoutMs: ctx.defaultToolTimeoutMs,
1594
1601
  probeTimeoutMs: ctx.probeTimeoutMs,
1595
1602
  activity: ctx.activity,
1603
+ defer: ctx.defer,
1596
1604
  });
1597
1605
 
1598
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
@@ -18,6 +18,7 @@ import {
18
18
  type CredentialHealthConfig,
19
19
  type CredentialHealthRecord,
20
20
  } from "./credential-health.js";
21
+ import type { DeferredWork } from "./connector-scope.js";
21
22
  import { splitAddress, type Toolkit } from "./toolkits.js";
22
23
 
23
24
  const ID_RE = /^[a-z0-9_-]+$/;
@@ -618,8 +619,9 @@ export class Registry implements RegistryView {
618
619
  checkCredentialHealth(
619
620
  baseUrl: string,
620
621
  opts?: CredentialCheckOptions,
622
+ defer?: DeferredWork,
621
623
  ): Promise<CredentialCheckResult[]> {
622
- return this.credentialHealth.check(baseUrl, opts);
624
+ return this.credentialHealth.check(baseUrl, opts, defer);
623
625
  }
624
626
 
625
627
  /**
@@ -630,8 +632,9 @@ export class Registry implements RegistryView {
630
632
  */
631
633
  sweepCredentialHealthIfDue(
632
634
  baseUrl: string,
635
+ defer?: DeferredWork,
633
636
  ): Promise<CredentialCheckResult[]> | undefined {
634
- return this.credentialHealth.sweepIfDue(baseUrl);
637
+ return this.credentialHealth.sweepIfDue(baseUrl, defer);
635
638
  }
636
639
 
637
640
  /**
package/src/server.ts CHANGED
@@ -897,6 +897,9 @@ async function serveMcp(
897
897
  activity,
898
898
  defaultToolTimeoutMs: opts.defaultToolTimeoutMs,
899
899
  probeTimeoutMs: opts.probeTimeoutMs,
900
+ ...(runtimeContext
901
+ ? { defer: runtimeContext.waitUntil.bind(runtimeContext) }
902
+ : {}),
900
903
  });
901
904
  if (opts.executor) {
902
905
  registerExecuteTool(server, registry, {
@@ -904,6 +907,7 @@ async function serveMcp(
904
907
  executor: opts.executor,
905
908
  logger: opts.logger,
906
909
  activity,
910
+ requestSignal: request.signal,
907
911
  });
908
912
  }
909
913
  const transport = new WebStandardStreamableHTTPServerTransport({
@@ -1045,6 +1049,9 @@ export function createFetchHandler(
1045
1049
  const url = new URL(request.url);
1046
1050
  const baseUrl = publicUrl ?? url.origin;
1047
1051
  const path = url.pathname;
1052
+ const defer = runtimeContext
1053
+ ? runtimeContext.waitUntil.bind(runtimeContext)
1054
+ : undefined;
1048
1055
 
1049
1056
  /**
1050
1057
  * Piggyback a DUE credential liveness sweep on traffic that has already been
@@ -1060,7 +1067,7 @@ export function createFetchHandler(
1060
1067
  // throws while deciding whether anything is due. Nothing about a
1061
1068
  // background health check may turn a served request into a 500.
1062
1069
  try {
1063
- const sweep = registry.sweepCredentialHealthIfDue(baseUrl);
1070
+ const sweep = registry.sweepCredentialHealthIfDue(baseUrl, defer);
1064
1071
  if (!sweep) return;
1065
1072
  const settled = sweep.then(
1066
1073
  () => {},
@@ -1068,7 +1075,7 @@ export function createFetchHandler(
1068
1075
  opts.logger.warn("[connecta] credential health sweep failed", err);
1069
1076
  },
1070
1077
  );
1071
- if (runtimeContext?.waitUntil) runtimeContext.waitUntil(settled);
1078
+ if (defer) defer(settled);
1072
1079
  else void settled;
1073
1080
  } catch (err) {
1074
1081
  opts.logger.warn("[connecta] credential health sweep failed", err);
@@ -1256,6 +1263,7 @@ export function createFetchHandler(
1256
1263
  Boolean(opts.activity?.list),
1257
1264
  credentialManagement,
1258
1265
  opts.toolkits,
1266
+ defer,
1259
1267
  );
1260
1268
  return privateJson(data);
1261
1269
  }
package/src/types.ts CHANGED
@@ -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,7 +4,10 @@ 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";
9
12
  import type { Toolkit } from "./toolkits.js";
10
13
  import type { ConnectaBranding, UiAuthConfig } from "./types.js";
@@ -404,6 +407,7 @@ export async function buildUiData(
404
407
  ? "available"
405
408
  : "requires_clerk",
406
409
  toolkits?: ReadonlyMap<string, Toolkit>,
410
+ defer?: DeferredWork,
407
411
  ): Promise<UiData> {
408
412
  const requestScope = {};
409
413
  const connectorSet = registry.listConnectors();
@@ -549,6 +553,7 @@ export async function buildUiData(
549
553
  closeConnectorScope(
550
554
  connector,
551
555
  registry.contextFor(connector.id, baseUrl, requestScope),
556
+ defer,
552
557
  ),
553
558
  ),
554
559
  );
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.3";
7
+ export const CONNECTA_VERSION = "0.7.4";