@zackbart/connecta 0.7.3 → 0.7.5
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.
- package/CHANGELOG.md +68 -0
- package/README.md +6 -3
- package/dist/connector-scope.d.ts +7 -4
- package/dist/connector-scope.d.ts.map +1 -1
- package/dist/connector-scope.js +36 -16
- package/dist/connector-scope.js.map +1 -1
- package/dist/connectors/remote-mcp.d.ts.map +1 -1
- package/dist/connectors/remote-mcp.js +47 -16
- package/dist/connectors/remote-mcp.js.map +1 -1
- package/dist/credential-health.d.ts +3 -2
- package/dist/credential-health.d.ts.map +1 -1
- package/dist/credential-health.js +9 -9
- package/dist/credential-health.js.map +1 -1
- package/dist/execute.d.ts +3 -0
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +70 -29
- package/dist/execute.js.map +1 -1
- package/dist/executor-admission.d.ts +53 -0
- package/dist/executor-admission.d.ts.map +1 -0
- package/dist/executor-admission.js +151 -0
- package/dist/executor-admission.js.map +1 -0
- package/dist/executor-result.d.ts +13 -0
- package/dist/executor-result.d.ts.map +1 -0
- package/dist/executor-result.js +57 -0
- package/dist/executor-result.js.map +1 -0
- package/dist/executors/quickjs-child.d.ts +2 -0
- package/dist/executors/quickjs-child.d.ts.map +1 -0
- package/dist/executors/quickjs-child.js +131 -0
- package/dist/executors/quickjs-child.js.map +1 -0
- package/dist/executors/quickjs-protocol.d.ts +55 -0
- package/dist/executors/quickjs-protocol.d.ts.map +1 -0
- package/dist/executors/quickjs-protocol.js +22 -0
- package/dist/executors/quickjs-protocol.js.map +1 -0
- package/dist/executors/quickjs-runtime.d.ts +26 -0
- package/dist/executors/quickjs-runtime.d.ts.map +1 -0
- package/dist/executors/quickjs-runtime.js +383 -0
- package/dist/executors/quickjs-runtime.js.map +1 -0
- package/dist/executors/quickjs.d.ts +20 -15
- package/dist/executors/quickjs.d.ts.map +1 -1
- package/dist/executors/quickjs.js +459 -311
- package/dist/executors/quickjs.js.map +1 -1
- package/dist/index.d.ts +13 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/meta-tools.d.ts +4 -0
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +3 -2
- package/dist/meta-tools.js.map +1 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +36 -3
- package/dist/node.js.map +1 -1
- package/dist/registry.d.ts +3 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +4 -4
- package/dist/registry.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +11 -4
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +2 -1
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +3 -3
- package/dist/ui.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/connector-scope.ts +43 -18
- package/src/connectors/remote-mcp.ts +56 -19
- package/src/credential-health.ts +20 -6
- package/src/execute.ts +93 -35
- package/src/executor-admission.ts +229 -0
- package/src/executor-result.ts +63 -0
- package/src/executors/quickjs-child.ts +168 -0
- package/src/executors/quickjs-protocol.ts +77 -0
- package/src/executors/quickjs-runtime.ts +440 -0
- package/src/executors/quickjs.ts +660 -353
- package/src/index.ts +28 -4
- package/src/meta-tools.ts +9 -1
- package/src/node.ts +36 -2
- package/src/registry.ts +5 -2
- package/src/server.ts +10 -2
- package/src/types.ts +17 -0
- package/src/ui.ts +6 -1
- 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) {
|
|
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(
|
|
536
|
-
|
|
537
|
-
|
|
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 {
|
|
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(
|
|
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 (
|
|
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 {
|
|
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