@zackbart/connecta 0.7.0 → 0.7.3
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 +110 -0
- package/README.md +2 -1
- package/dist/connectors/remote-mcp.d.ts +24 -1
- package/dist/connectors/remote-mcp.d.ts.map +1 -1
- package/dist/connectors/remote-mcp.js +208 -88
- package/dist/connectors/remote-mcp.js.map +1 -1
- package/dist/credential-health.d.ts +8 -5
- package/dist/credential-health.d.ts.map +1 -1
- package/dist/credential-health.js +20 -13
- package/dist/credential-health.js.map +1 -1
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +10 -8
- package/dist/execute.js.map +1 -1
- package/dist/executors/quickjs.d.ts.map +1 -1
- package/dist/executors/quickjs.js +57 -5
- package/dist/executors/quickjs.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/meta-tools.d.ts +25 -0
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +162 -20
- package/dist/meta-tools.js.map +1 -1
- package/dist/registry.d.ts +18 -22
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +33 -21
- package/dist/registry.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +18 -7
- package/dist/server.js.map +1 -1
- package/dist/timeout.d.ts +9 -4
- package/dist/timeout.d.ts.map +1 -1
- package/dist/timeout.js +34 -4
- package/dist/timeout.js.map +1 -1
- package/dist/toolkits.d.ts +8 -0
- package/dist/toolkits.d.ts.map +1 -1
- package/dist/toolkits.js +3 -0
- package/dist/toolkits.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +12 -1
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +187 -6
- 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/connectors/remote-mcp.ts +269 -93
- package/src/credential-health.ts +20 -18
- package/src/execute.ts +18 -7
- package/src/executors/quickjs.ts +65 -5
- package/src/index.ts +7 -2
- package/src/meta-tools.ts +226 -43
- package/src/registry.ts +48 -20
- package/src/server.ts +20 -9
- package/src/timeout.ts +41 -4
- package/src/toolkits.ts +11 -0
- package/src/types.ts +2 -2
- package/src/ui.ts +212 -11
- package/src/version.ts +1 -1
package/src/registry.ts
CHANGED
|
@@ -6,7 +6,11 @@ import type {
|
|
|
6
6
|
Logger,
|
|
7
7
|
ToolDef,
|
|
8
8
|
} from "./types.js";
|
|
9
|
-
import
|
|
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,
|
|
@@ -154,6 +158,11 @@ function msg(err: unknown): string {
|
|
|
154
158
|
return err instanceof Error ? err.message : String(err);
|
|
155
159
|
}
|
|
156
160
|
|
|
161
|
+
type ConnectorOperationOptions = Pick<
|
|
162
|
+
ConnectorContext,
|
|
163
|
+
"signal" | "timeoutMs"
|
|
164
|
+
>;
|
|
165
|
+
|
|
157
166
|
/**
|
|
158
167
|
* The registry surface a per-connection MCP server consumes: every meta-tool
|
|
159
168
|
* (`src/meta-tools.ts`) and the `execute_code` sandbox bridge (`src/execute.ts`)
|
|
@@ -178,18 +187,20 @@ export interface RegistryView {
|
|
|
178
187
|
id: string,
|
|
179
188
|
baseUrl: string,
|
|
180
189
|
requestScope?: object,
|
|
190
|
+
callOptions?: ConnectorOperationOptions,
|
|
181
191
|
): Promise<ToolDef[]>;
|
|
182
192
|
refreshTools(
|
|
183
193
|
id: string,
|
|
184
194
|
baseUrl: string,
|
|
185
195
|
requestScope?: object,
|
|
196
|
+
callOptions?: ConnectorOperationOptions,
|
|
186
197
|
): Promise<ToolDef[]>;
|
|
187
198
|
peekTools(id: string): ToolDef[] | undefined;
|
|
188
199
|
contextFor(
|
|
189
200
|
id: string,
|
|
190
201
|
baseUrl: string,
|
|
191
202
|
requestScope?: object,
|
|
192
|
-
callOptions?:
|
|
203
|
+
callOptions?: ConnectorOperationOptions,
|
|
193
204
|
): ConnectorContext;
|
|
194
205
|
resultsStorage(): KVStorage;
|
|
195
206
|
recordSuccess(id: string, latencyMs: number): void;
|
|
@@ -209,6 +220,7 @@ export interface RegistryView {
|
|
|
209
220
|
id: string,
|
|
210
221
|
baseUrl: string,
|
|
211
222
|
requestScope?: object,
|
|
223
|
+
callOptions?: ConnectorOperationOptions,
|
|
212
224
|
): Promise<ConnectorStatus>;
|
|
213
225
|
invalidateStored(id: string): Promise<void>;
|
|
214
226
|
}
|
|
@@ -352,21 +364,31 @@ export class Registry implements RegistryView {
|
|
|
352
364
|
id: string,
|
|
353
365
|
baseUrl: string,
|
|
354
366
|
requestScope: object = {},
|
|
355
|
-
callOptions:
|
|
367
|
+
callOptions: ConnectorOperationOptions = {},
|
|
356
368
|
): ConnectorContext {
|
|
369
|
+
const credentialConfig = this.connectors.get(id)?.credential;
|
|
370
|
+
let credentialAccess: ConnectorContext["credential"];
|
|
371
|
+
if (this.opts.credentialVault && credentialConfig) {
|
|
372
|
+
const vault = this.opts.credentialVault;
|
|
373
|
+
const readValues = async () => {
|
|
374
|
+
const values = await vault.getAll(id);
|
|
375
|
+
const shape = storedCredentialShape(credentialConfig, values);
|
|
376
|
+
if (shape.state === "mismatch") {
|
|
377
|
+
throw new ConnectorCallError("auth_required", shape.message);
|
|
378
|
+
}
|
|
379
|
+
return values;
|
|
380
|
+
};
|
|
381
|
+
credentialAccess = {
|
|
382
|
+
get: async (field = "value") =>
|
|
383
|
+
(await readValues())?.[field] ?? null,
|
|
384
|
+
getAll: readValues,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
357
387
|
return {
|
|
358
388
|
storage: namespaced(this.opts.storage, `conn:${id}:`),
|
|
359
389
|
logger: this.opts.logger,
|
|
360
390
|
baseUrl,
|
|
361
|
-
...(
|
|
362
|
-
? {
|
|
363
|
-
credential: {
|
|
364
|
-
get: (field?: string) =>
|
|
365
|
-
this.opts.credentialVault!.get(id, field),
|
|
366
|
-
getAll: () => this.opts.credentialVault!.getAll(id),
|
|
367
|
-
},
|
|
368
|
-
}
|
|
369
|
-
: {}),
|
|
391
|
+
...(credentialAccess ? { credential: credentialAccess } : {}),
|
|
370
392
|
requestScope,
|
|
371
393
|
...callOptions,
|
|
372
394
|
};
|
|
@@ -441,13 +463,14 @@ export class Registry implements RegistryView {
|
|
|
441
463
|
id: string,
|
|
442
464
|
baseUrl: string,
|
|
443
465
|
requestScope?: object,
|
|
466
|
+
callOptions: ConnectorOperationOptions = {},
|
|
444
467
|
): Promise<ToolDef[]> {
|
|
445
468
|
const connector = this.connectors.get(id);
|
|
446
469
|
if (!connector) throw new Error(`Unknown connector "${id}"`);
|
|
447
470
|
const tools = connector.staticTools
|
|
448
471
|
? connector.staticTools
|
|
449
472
|
: await connector.listTools(
|
|
450
|
-
this.contextFor(id, baseUrl, requestScope),
|
|
473
|
+
this.contextFor(id, baseUrl, requestScope, callOptions),
|
|
451
474
|
);
|
|
452
475
|
const now = Date.now();
|
|
453
476
|
const previous = this.cache.get(id);
|
|
@@ -479,6 +502,7 @@ export class Registry implements RegistryView {
|
|
|
479
502
|
id: string,
|
|
480
503
|
baseUrl: string,
|
|
481
504
|
requestScope?: object,
|
|
505
|
+
callOptions: ConnectorOperationOptions = {},
|
|
482
506
|
): Promise<ToolDef[]> {
|
|
483
507
|
const connector = this.connectors.get(id);
|
|
484
508
|
if (!connector) throw new Error(`Unknown connector "${id}"`);
|
|
@@ -512,7 +536,7 @@ export class Registry implements RegistryView {
|
|
|
512
536
|
}
|
|
513
537
|
|
|
514
538
|
try {
|
|
515
|
-
return await this.refreshTools(id, baseUrl, requestScope);
|
|
539
|
+
return await this.refreshTools(id, baseUrl, requestScope, callOptions);
|
|
516
540
|
} catch (err) {
|
|
517
541
|
if (stale) {
|
|
518
542
|
this.opts.logger.warn(
|
|
@@ -626,10 +650,11 @@ export class Registry implements RegistryView {
|
|
|
626
650
|
id: string,
|
|
627
651
|
baseUrl: string,
|
|
628
652
|
requestScope: object = {},
|
|
653
|
+
callOptions: ConnectorOperationOptions = {},
|
|
629
654
|
): Promise<ConnectorStatus> {
|
|
630
655
|
const connector = this.connectors.get(id);
|
|
631
656
|
if (!connector) return { state: "error", message: "Unknown connector" };
|
|
632
|
-
const ctx = this.contextFor(id, baseUrl, requestScope);
|
|
657
|
+
const ctx = this.contextFor(id, baseUrl, requestScope, callOptions);
|
|
633
658
|
if (connector.status) {
|
|
634
659
|
try {
|
|
635
660
|
return await connector.status(ctx);
|
|
@@ -638,7 +663,7 @@ export class Registry implements RegistryView {
|
|
|
638
663
|
}
|
|
639
664
|
}
|
|
640
665
|
try {
|
|
641
|
-
await this.getTools(id, baseUrl, requestScope);
|
|
666
|
+
await this.getTools(id, baseUrl, requestScope, callOptions);
|
|
642
667
|
return { state: "ok" };
|
|
643
668
|
} catch (err) {
|
|
644
669
|
return { state: "error", message: msg(err) };
|
|
@@ -782,11 +807,12 @@ export class ScopedRegistry implements RegistryView {
|
|
|
782
807
|
id: string,
|
|
783
808
|
baseUrl: string,
|
|
784
809
|
requestScope?: object,
|
|
810
|
+
callOptions: ConnectorOperationOptions = {},
|
|
785
811
|
): Promise<ToolDef[]> {
|
|
786
812
|
if (!this.visible(id)) throw this.unknownConnector(id);
|
|
787
813
|
return this.inScopeTools(
|
|
788
814
|
id,
|
|
789
|
-
await this.base.getTools(id, baseUrl, requestScope),
|
|
815
|
+
await this.base.getTools(id, baseUrl, requestScope, callOptions),
|
|
790
816
|
);
|
|
791
817
|
}
|
|
792
818
|
|
|
@@ -794,11 +820,12 @@ export class ScopedRegistry implements RegistryView {
|
|
|
794
820
|
id: string,
|
|
795
821
|
baseUrl: string,
|
|
796
822
|
requestScope?: object,
|
|
823
|
+
callOptions: ConnectorOperationOptions = {},
|
|
797
824
|
): Promise<ToolDef[]> {
|
|
798
825
|
if (!this.visible(id)) throw this.unknownConnector(id);
|
|
799
826
|
return this.inScopeTools(
|
|
800
827
|
id,
|
|
801
|
-
await this.base.refreshTools(id, baseUrl, requestScope),
|
|
828
|
+
await this.base.refreshTools(id, baseUrl, requestScope, callOptions),
|
|
802
829
|
);
|
|
803
830
|
}
|
|
804
831
|
|
|
@@ -812,7 +839,7 @@ export class ScopedRegistry implements RegistryView {
|
|
|
812
839
|
id: string,
|
|
813
840
|
baseUrl: string,
|
|
814
841
|
requestScope: object = {},
|
|
815
|
-
callOptions:
|
|
842
|
+
callOptions: ConnectorOperationOptions = {},
|
|
816
843
|
): ConnectorContext {
|
|
817
844
|
// Unreachable through the meta-tools (they resolve first), so a throw here
|
|
818
845
|
// is a loud backstop rather than a silent grant of connector storage and
|
|
@@ -897,12 +924,13 @@ export class ScopedRegistry implements RegistryView {
|
|
|
897
924
|
id: string,
|
|
898
925
|
baseUrl: string,
|
|
899
926
|
requestScope: object = {},
|
|
927
|
+
callOptions: { signal?: AbortSignal; timeoutMs?: number } = {},
|
|
900
928
|
): Promise<ConnectorStatus> {
|
|
901
929
|
// Same shape the unscoped registry returns for an unregistered id.
|
|
902
930
|
if (!this.visible(id)) {
|
|
903
931
|
return { state: "error", message: "Unknown connector" };
|
|
904
932
|
}
|
|
905
|
-
return this.base.statusFor(id, baseUrl, requestScope);
|
|
933
|
+
return this.base.statusFor(id, baseUrl, requestScope, callOptions);
|
|
906
934
|
}
|
|
907
935
|
|
|
908
936
|
async invalidateStored(id: string): Promise<void> {
|
package/src/server.ts
CHANGED
|
@@ -222,8 +222,8 @@ async function authorize(
|
|
|
222
222
|
| {
|
|
223
223
|
ok: true;
|
|
224
224
|
actor: ActivityActor;
|
|
225
|
-
|
|
226
|
-
|
|
225
|
+
/** True only when the admitting provider can also authorize UI mutation. */
|
|
226
|
+
uiAdminEligible?: boolean;
|
|
227
227
|
/** The admitting identity's toolkit binding (docs/toolkits.md). */
|
|
228
228
|
toolkitBinding?: ToolkitBinding;
|
|
229
229
|
}
|
|
@@ -261,8 +261,9 @@ async function authorize(
|
|
|
261
261
|
kind: provider.kind,
|
|
262
262
|
...(subjectId ? { id: subjectId } : {}),
|
|
263
263
|
},
|
|
264
|
-
|
|
265
|
-
|
|
264
|
+
...(result.userId && provider.uiAuth?.kind === "clerk"
|
|
265
|
+
? { uiAdminEligible: true }
|
|
266
|
+
: {}),
|
|
266
267
|
...(binding.binding ? { toolkitBinding: binding.binding } : {}),
|
|
267
268
|
};
|
|
268
269
|
}
|
|
@@ -1088,7 +1089,15 @@ export function createFetchHandler(
|
|
|
1088
1089
|
// Canonicalize the legacy bookmark while upgrading it so an old /ui URL
|
|
1089
1090
|
// reaches the new Connections entry point in one permanent redirect.
|
|
1090
1091
|
const targetPath = path === "/ui" ? "/" : url.pathname;
|
|
1091
|
-
|
|
1092
|
+
// Assign the path and query onto the configured URL instead of resolving
|
|
1093
|
+
// attacker-controlled text against it. A pathname beginning with `//`
|
|
1094
|
+
// (including a backslash form normalized by URL parsing) is an authority
|
|
1095
|
+
// when passed to `new URL(value, base)` and would otherwise replace the
|
|
1096
|
+
// deployment host.
|
|
1097
|
+
const target = new URL(publicUrl);
|
|
1098
|
+
target.pathname = targetPath;
|
|
1099
|
+
target.search = url.search;
|
|
1100
|
+
target.hash = "";
|
|
1092
1101
|
return withSecurityHeaders(
|
|
1093
1102
|
new Response(null, {
|
|
1094
1103
|
status: 308,
|
|
@@ -1189,7 +1198,7 @@ export function createFetchHandler(
|
|
|
1189
1198
|
|
|
1190
1199
|
const operatorPage = operatorPageForPath(path);
|
|
1191
1200
|
if (operatorPage) {
|
|
1192
|
-
if (request.method !== "GET") {
|
|
1201
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
1193
1202
|
return privateJson({ error: "method not allowed" }, { status: 405 });
|
|
1194
1203
|
}
|
|
1195
1204
|
// Open shell — carries no operator data; everything comes from the
|
|
@@ -1204,7 +1213,9 @@ export function createFetchHandler(
|
|
|
1204
1213
|
// — only script execution, the XSS sink, is gated.
|
|
1205
1214
|
const nonce = uiScriptNonce();
|
|
1206
1215
|
return new Response(
|
|
1207
|
-
|
|
1216
|
+
request.method === "HEAD"
|
|
1217
|
+
? null
|
|
1218
|
+
: renderUiHtml(uiAuth, mcpUrl, opts.branding, nonce, operatorPage),
|
|
1208
1219
|
{
|
|
1209
1220
|
status: 200,
|
|
1210
1221
|
headers: {
|
|
@@ -1227,8 +1238,7 @@ export function createFetchHandler(
|
|
|
1227
1238
|
// After the restriction check, not before: an identity that may not
|
|
1228
1239
|
// read this surface should not get to trigger background work from it.
|
|
1229
1240
|
sweepCredentials();
|
|
1230
|
-
const eligibleClerkOperator =
|
|
1231
|
-
authz.providerKind === "clerk" && Boolean(authz.userId);
|
|
1241
|
+
const eligibleClerkOperator = authz.uiAdminEligible === true;
|
|
1232
1242
|
const credentialManagement = credentialManagementCapability({
|
|
1233
1243
|
eligibleClerkOperator,
|
|
1234
1244
|
hasCredentialSlots: registry
|
|
@@ -1245,6 +1255,7 @@ export function createFetchHandler(
|
|
|
1245
1255
|
eligibleClerkOperator ? opts.credentialVault : undefined,
|
|
1246
1256
|
Boolean(opts.activity?.list),
|
|
1247
1257
|
credentialManagement,
|
|
1258
|
+
opts.toolkits,
|
|
1248
1259
|
);
|
|
1249
1260
|
return privateJson(data);
|
|
1250
1261
|
}
|
package/src/timeout.ts
CHANGED
|
@@ -21,10 +21,9 @@ export function normalizeTimeoutMs(
|
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Reject `promise` after `ms` if it has not settled, so one hung downstream
|
|
24
|
-
* cannot stall a whole fan-out.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* (AbortSignal plumbed through the registry) is a deferred follow-up.
|
|
24
|
+
* cannot stall a whole fan-out. This form bounds only the caller-facing wait;
|
|
25
|
+
* use `withAbortableTimeout` when the operation accepts an AbortSignal and the
|
|
26
|
+
* underlying work must stop too.
|
|
28
27
|
*/
|
|
29
28
|
export function withTimeout<T>(
|
|
30
29
|
promise: Promise<T>,
|
|
@@ -47,3 +46,41 @@ export function withTimeout<T>(
|
|
|
47
46
|
);
|
|
48
47
|
});
|
|
49
48
|
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Give one operation a caller-facing deadline and the matching cancellation
|
|
52
|
+
* signal. The timeout rejects with the stable, labelled error while aborting
|
|
53
|
+
* any in-flight work that honors the signal.
|
|
54
|
+
*/
|
|
55
|
+
export function withAbortableTimeout<T>(
|
|
56
|
+
operation: (signal: AbortSignal) => Promise<T>,
|
|
57
|
+
ms: number,
|
|
58
|
+
label: string,
|
|
59
|
+
): Promise<T> {
|
|
60
|
+
const controller = new AbortController();
|
|
61
|
+
return new Promise<T>((resolve, reject) => {
|
|
62
|
+
const timeoutError = new Error(`${label} timed out after ${ms}ms`);
|
|
63
|
+
const timer = setTimeout(() => {
|
|
64
|
+
controller.abort(timeoutError);
|
|
65
|
+
reject(timeoutError);
|
|
66
|
+
}, ms);
|
|
67
|
+
let promise: Promise<T>;
|
|
68
|
+
try {
|
|
69
|
+
promise = operation(controller.signal);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
reject(err);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
promise.then(
|
|
76
|
+
(value) => {
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
resolve(value);
|
|
79
|
+
},
|
|
80
|
+
(err) => {
|
|
81
|
+
clearTimeout(timer);
|
|
82
|
+
reject(err);
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
}
|
package/src/toolkits.ts
CHANGED
|
@@ -41,6 +41,14 @@ export type ToolkitConfig = Record<string, ToolkitDefinition>;
|
|
|
41
41
|
export interface Toolkit {
|
|
42
42
|
readonly name: string;
|
|
43
43
|
readonly description?: string;
|
|
44
|
+
/**
|
|
45
|
+
* The validated config this scope came from. Kept as immutable data so
|
|
46
|
+
* deployment-wide operator surfaces can explain the running configuration
|
|
47
|
+
* without inventing a second source of truth.
|
|
48
|
+
*/
|
|
49
|
+
readonly connectors: readonly string[];
|
|
50
|
+
readonly includeTools: readonly string[];
|
|
51
|
+
readonly excludeTools: readonly string[];
|
|
44
52
|
/** True when `connectorId` is inside this toolkit's scope. */
|
|
45
53
|
hasConnector(connectorId: string): boolean;
|
|
46
54
|
/** True when `<connectorId>.<toolName>` is inside this toolkit's scope. */
|
|
@@ -178,6 +186,9 @@ function resolveToolkit(
|
|
|
178
186
|
return {
|
|
179
187
|
name,
|
|
180
188
|
...(definition.description ? { description: definition.description } : {}),
|
|
189
|
+
connectors: Object.freeze([...connectorIds]),
|
|
190
|
+
includeTools: Object.freeze([...(definition.includeTools ?? [])]),
|
|
191
|
+
excludeTools: Object.freeze([...(definition.excludeTools ?? [])]),
|
|
181
192
|
hasConnector: (connectorId) => connectorIds.has(connectorId),
|
|
182
193
|
hasTool: (connectorId, toolName) => {
|
|
183
194
|
if (!connectorIds.has(connectorId)) return false;
|
package/src/types.ts
CHANGED
|
@@ -113,9 +113,9 @@ export interface ConnectorContext {
|
|
|
113
113
|
* when omitted.
|
|
114
114
|
*/
|
|
115
115
|
requestScope?: object;
|
|
116
|
-
/** Best-effort cancellation signal for this
|
|
116
|
+
/** Best-effort cancellation signal for this connector operation. */
|
|
117
117
|
signal?: AbortSignal;
|
|
118
|
-
/** Requested
|
|
118
|
+
/** Requested connector-operation deadline in milliseconds. */
|
|
119
119
|
timeoutMs?: number;
|
|
120
120
|
}
|
|
121
121
|
|