@zackbart/connecta 0.24.1 → 0.24.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 +169 -0
- package/dist/auth/bearer.js +2 -0
- package/dist/auth/clerk.d.ts +0 -5
- package/dist/auth/clerk.js +21 -8
- package/dist/auth/downstream-oauth.d.ts +12 -1
- package/dist/auth/downstream-oauth.js +147 -35
- package/dist/call-admission.d.ts +4 -0
- package/dist/call-admission.js +26 -0
- package/dist/catalog-drift.js +9 -4
- package/dist/catalog-service.d.ts +2 -0
- package/dist/catalog-service.js +25 -8
- package/dist/catalog.d.ts +2 -0
- package/dist/catalog.js +246 -121
- package/dist/connector-access.d.ts +32 -0
- package/dist/connector-access.js +79 -0
- package/dist/connectors/api.js +11 -1
- package/dist/connectors/guarded-fetch.d.ts +1 -1
- package/dist/connectors/guarded-fetch.js +27 -20
- package/dist/connectors/remote-mcp.js +84 -53
- package/dist/errors.d.ts +17 -0
- package/dist/errors.js +58 -0
- package/dist/execute.js +85 -23
- package/dist/executor-result.js +3 -1
- package/dist/executors/quickjs-child.js +5 -1
- package/dist/executors/quickjs-protocol.d.ts +4 -0
- package/dist/executors/quickjs-runtime.d.ts +1 -1
- package/dist/executors/quickjs-runtime.js +38 -21
- package/dist/executors/quickjs.js +68 -27
- package/dist/index.d.ts +37 -1
- package/dist/index.js +89 -3
- package/dist/invocation.js +134 -93
- package/dist/mcp-result.js +3 -2
- package/dist/meta-tools.js +118 -39
- package/dist/registry.d.ts +29 -1
- package/dist/registry.js +122 -15
- package/dist/routes/credentials.js +1 -0
- package/dist/routes/mcp.d.ts +4 -1
- package/dist/routes/mcp.js +112 -12
- package/dist/routes/oauth-management.js +1 -0
- package/dist/routes/oauth.js +4 -0
- package/dist/routes/shared.d.ts +7 -1
- package/dist/routes/shared.js +12 -13
- package/dist/routes/ui.js +2 -1
- package/dist/server.js +15 -3
- package/dist/skills.js +6 -5
- package/dist/storage/file.d.ts +6 -2
- package/dist/storage/file.js +312 -34
- package/dist/storage/memory.js +12 -1
- package/dist/validate.js +3 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +30 -9
- package/documentation/auth.md +110 -6
- package/documentation/call-admission.md +24 -8
- package/documentation/code-mode.md +34 -22
- package/documentation/connectors.md +47 -5
- package/documentation/meta-tools.md +74 -6
- package/documentation/operations.md +20 -19
- package/documentation/provider-conventions.md +7 -0
- package/documentation/request-admission.md +38 -4
- package/documentation/storage-and-credentials.md +54 -1
- package/documentation/upgrading.md +21 -5
- package/ethos.md +1 -1
- package/package.json +1 -1
- package/templates/node/package.json +1 -1
package/dist/routes/mcp.js
CHANGED
|
@@ -2,9 +2,10 @@ import { createMcpHandler, isLegacyRequest, McpServer, WebStandardStreamableHTTP
|
|
|
2
2
|
import { registerExecuteTool } from "../execute.js";
|
|
3
3
|
import { ExecutorAdmissionError, } from "../executor-admission.js";
|
|
4
4
|
import { registerMetaTools } from "../meta-tools.js";
|
|
5
|
+
import { intersectAccess } from "../connector-access.js";
|
|
5
6
|
import { instructionsFor } from "../skills.js";
|
|
6
7
|
import { msg } from "../errors.js";
|
|
7
|
-
import { authorize, mayManageConnector, validateAuthPermissions, } from "./shared.js";
|
|
8
|
+
import { authorize, loggableValue, mayManageConnector, validateAuthPermissions, } from "./shared.js";
|
|
8
9
|
export const MCP_CORS_HEADERS = {
|
|
9
10
|
"Access-Control-Allow-Origin": "*",
|
|
10
11
|
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
|
|
@@ -13,11 +14,26 @@ export const MCP_CORS_HEADERS = {
|
|
|
13
14
|
// Browser-based MCP clients call /mcp cross-origin. Without CORS on every
|
|
14
15
|
// response — errors included — the browser hides the 401, the client cannot
|
|
15
16
|
// read WWW-Authenticate, and OAuth discovery silently never starts.
|
|
16
|
-
function withMcpCors(response) {
|
|
17
|
+
function withMcpCors(response, request, allowedOrigin) {
|
|
17
18
|
const headers = new Headers(response.headers);
|
|
18
19
|
for (const [name, value] of Object.entries(MCP_CORS_HEADERS)) {
|
|
19
20
|
headers.set(name, value);
|
|
20
21
|
}
|
|
22
|
+
headers.delete("Access-Control-Allow-Origin");
|
|
23
|
+
if (allowedOrigin !== null)
|
|
24
|
+
headers.set("Access-Control-Allow-Origin", allowedOrigin);
|
|
25
|
+
headers.append("Vary", "Origin");
|
|
26
|
+
if (request.method === "OPTIONS") {
|
|
27
|
+
// Browsers do not interpret a prefix wildcard in Allow-Headers. Echo only
|
|
28
|
+
// valid SEP-2243 field names; unrelated requested headers stay disallowed.
|
|
29
|
+
const paramHeaders = (request.headers.get("Access-Control-Request-Headers") ?? "")
|
|
30
|
+
.toLowerCase().split(",").map(name => name.trim())
|
|
31
|
+
.filter(name => /^mcp-param-[!#$%&'*+.^_`|~0-9a-z-]+$/.test(name));
|
|
32
|
+
if (paramHeaders.length) {
|
|
33
|
+
headers.append("Access-Control-Allow-Headers", [...new Set(paramHeaders)].join(", "));
|
|
34
|
+
}
|
|
35
|
+
headers.append("Vary", "Access-Control-Request-Headers");
|
|
36
|
+
}
|
|
21
37
|
headers.set("Access-Control-Expose-Headers", "WWW-Authenticate, Retry-After, mcp-session-id, mcp-protocol-version");
|
|
22
38
|
return new Response(response.body, {
|
|
23
39
|
status: response.status,
|
|
@@ -45,7 +61,10 @@ function requestAdmissionFailure(error) {
|
|
|
45
61
|
jsonrpc: "2.0",
|
|
46
62
|
id: null,
|
|
47
63
|
error: {
|
|
48
|
-
|
|
64
|
+
// MCP 2026-07-28 basic#error-codes forbids new allocations in the
|
|
65
|
+
// legacy -32000..-32019 range. Use application codes outside the
|
|
66
|
+
// JSON-RPC reserved range, avoiding retired protocol meanings.
|
|
67
|
+
code: overloaded ? -31001 : -31002,
|
|
49
68
|
message: overloaded
|
|
50
69
|
? "Server capacity is exhausted. Retry later."
|
|
51
70
|
: "Server is shutting down.",
|
|
@@ -243,6 +262,49 @@ async function serveMcp(request, opts, baseUrl, actor, registry, canManageAuth,
|
|
|
243
262
|
return transport.handleRequest(request);
|
|
244
263
|
}
|
|
245
264
|
export function createMcpRoute(opts) {
|
|
265
|
+
const configuredOrigins = opts.allowedOrigins;
|
|
266
|
+
const isExactOrigin = (value) => {
|
|
267
|
+
if (typeof value !== "string")
|
|
268
|
+
return false;
|
|
269
|
+
try {
|
|
270
|
+
const url = new URL(value);
|
|
271
|
+
return (url.protocol === "http:" || url.protocol === "https:") && url.origin === value;
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
if (configuredOrigins !== undefined && configuredOrigins !== "*" &&
|
|
278
|
+
(!Array.isArray(configuredOrigins) || !configuredOrigins.every(isExactOrigin))) {
|
|
279
|
+
throw new TypeError('ConnectaConfig.allowedOrigins must be an array of exact HTTP(S) origins or "*".');
|
|
280
|
+
}
|
|
281
|
+
const origins = new Set(configuredOrigins === undefined
|
|
282
|
+
? opts.publicUrl ? [new URL(opts.publicUrl).origin] : []
|
|
283
|
+
: configuredOrigins === "*" ? [] : configuredOrigins);
|
|
284
|
+
const allowsOrigin = (origin) => {
|
|
285
|
+
if (configuredOrigins === "*")
|
|
286
|
+
return true;
|
|
287
|
+
if (!isExactOrigin(origin))
|
|
288
|
+
return false;
|
|
289
|
+
if (origins.has(origin))
|
|
290
|
+
return true;
|
|
291
|
+
if (configuredOrigins !== undefined)
|
|
292
|
+
return false;
|
|
293
|
+
const hostname = new URL(origin).hostname;
|
|
294
|
+
return hostname === "localhost" || hostname === "[::1]" || /^127\.\d+\.\d+\.\d+$/.test(hostname);
|
|
295
|
+
};
|
|
296
|
+
const rejectOrigin = (request) => {
|
|
297
|
+
const path = new URL(request.url).pathname;
|
|
298
|
+
if (path !== "/mcp" && !path.startsWith("/mcp/"))
|
|
299
|
+
return null;
|
|
300
|
+
const origin = request.headers.get("Origin");
|
|
301
|
+
if (origin === null || allowsOrigin(origin))
|
|
302
|
+
return null;
|
|
303
|
+
return withMcpCors(new Response('{"error":"origin not allowed"}', {
|
|
304
|
+
status: 403,
|
|
305
|
+
headers: { "Content-Type": "application/json", "Cache-Control": "no-store" },
|
|
306
|
+
}), request, null);
|
|
307
|
+
};
|
|
246
308
|
let lastAdmissionWarningAt = 0;
|
|
247
309
|
let suppressedAdmissionWarnings = 0;
|
|
248
310
|
const warnAdmissionRejected = (error) => {
|
|
@@ -260,10 +322,21 @@ export function createMcpRoute(opts) {
|
|
|
260
322
|
lastAdmissionWarningAt = now;
|
|
261
323
|
suppressedAdmissionWarnings = 0;
|
|
262
324
|
};
|
|
263
|
-
|
|
325
|
+
async function routeMcp(context) {
|
|
264
326
|
const { path, request, baseUrl, runtimeContext, } = context;
|
|
265
|
-
if (path !== "/mcp")
|
|
327
|
+
if (path !== "/mcp" && !path.startsWith("/mcp/"))
|
|
266
328
|
return null;
|
|
329
|
+
const poolName = path === "/mcp" ? undefined : path.slice("/mcp/".length);
|
|
330
|
+
const origin = request.headers.get("Origin");
|
|
331
|
+
const allowed = origin === null || allowsOrigin(origin);
|
|
332
|
+
const cors = (response) => withMcpCors(response, request, configuredOrigins === "*" ? "*" : allowed ? origin : null);
|
|
333
|
+
// DNS-rebinding refusals cost neither a permit nor an auth lookup. This
|
|
334
|
+
// local header check also guards OPTIONS before any provider metadata.
|
|
335
|
+
const refusal = rejectOrigin(request);
|
|
336
|
+
if (refusal)
|
|
337
|
+
return refusal;
|
|
338
|
+
if (request.method === "OPTIONS")
|
|
339
|
+
return cors(new Response(null, { status: 204 }));
|
|
267
340
|
let admission;
|
|
268
341
|
try {
|
|
269
342
|
admission = await opts.requestAdmission.acquire({
|
|
@@ -286,38 +359,65 @@ export function createMcpRoute(opts) {
|
|
|
286
359
|
if (error.code === "executor_overloaded") {
|
|
287
360
|
warnAdmissionRejected(error);
|
|
288
361
|
}
|
|
289
|
-
return
|
|
362
|
+
return cors(requestAdmissionFailure(error));
|
|
290
363
|
}
|
|
291
364
|
throw error;
|
|
292
365
|
}
|
|
293
366
|
try {
|
|
294
367
|
const authz = await authorize(request, baseUrl, opts.auth, runtimeContext, opts.identity);
|
|
295
368
|
if (!authz.ok) {
|
|
296
|
-
return releaseAdmissionWithResponse(
|
|
369
|
+
return releaseAdmissionWithResponse(cors(authz.response), admission, request.signal);
|
|
370
|
+
}
|
|
371
|
+
// A pool endpoint narrows the identity's own view and nothing else. An
|
|
372
|
+
// undeclared name, a grant that refuses, and a grant that throws are
|
|
373
|
+
// one identical 404 so a credential never enumerates the other pools;
|
|
374
|
+
// the operator log is where the reason lives.
|
|
375
|
+
let access = authz;
|
|
376
|
+
if (poolName !== undefined) {
|
|
377
|
+
const pool = opts.pools?.get(poolName);
|
|
378
|
+
let granted = false;
|
|
379
|
+
let reason = "undeclared";
|
|
380
|
+
if (pool) {
|
|
381
|
+
try {
|
|
382
|
+
granted = (await pool.grant(authz.identity)) === true;
|
|
383
|
+
reason = granted ? "granted" : "refused";
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
reason = "grant threw";
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (!pool || !granted) {
|
|
390
|
+
opts.logger.warn(`[connecta] refused /mcp/${poolName} with 404: pool ${reason}` +
|
|
391
|
+
(authz.actor.id ? ` for ${loggableValue(authz.actor.id)}` : ""));
|
|
392
|
+
return releaseAdmissionWithResponse(cors(new Response("Not Found", { status: 404 })), admission, request.signal);
|
|
393
|
+
}
|
|
394
|
+
access = intersectAccess(authz, pool.access);
|
|
297
395
|
}
|
|
298
396
|
let scopedRegistry;
|
|
299
397
|
try {
|
|
300
398
|
validateAuthPermissions(authz, opts.registry);
|
|
301
399
|
scopedRegistry = opts.registry.scoped({
|
|
302
|
-
connectorIds:
|
|
400
|
+
connectorIds: access.connectorIds,
|
|
401
|
+
...(access.toolAccess ? { toolAccess: access.toolAccess } : {}),
|
|
303
402
|
...(authz.subjectKey ? { subjectKey: authz.subjectKey } : {}),
|
|
304
403
|
...(authz.principalKey ? { principalKey: authz.principalKey } : {}),
|
|
305
404
|
});
|
|
306
405
|
}
|
|
307
406
|
catch (error) {
|
|
308
|
-
return releaseAdmissionWithResponse(
|
|
407
|
+
return releaseAdmissionWithResponse(cors(new Response(JSON.stringify({ error: msg(error) }), {
|
|
309
408
|
status: 403,
|
|
310
409
|
headers: { "Content-Type": "application/json" },
|
|
311
410
|
})), admission, request.signal);
|
|
312
411
|
}
|
|
313
412
|
if (new URL(request.url).searchParams.has("toolkit")) {
|
|
314
|
-
return releaseAdmissionWithResponse(
|
|
413
|
+
return releaseAdmissionWithResponse(cors(toolkitRetired(opts.logger)), admission, request.signal);
|
|
315
414
|
}
|
|
316
|
-
return releaseAdmissionWithResponse(
|
|
415
|
+
return releaseAdmissionWithResponse(cors(await serveMcp(request, opts, baseUrl, authz.actor, scopedRegistry, id => { const connector = scopedRegistry.getConnector(id); return Boolean(connector && mayManageConnector(authz, connector)); }, runtimeContext)), admission, request.signal);
|
|
317
416
|
}
|
|
318
417
|
catch (error) {
|
|
319
418
|
admission.release();
|
|
320
419
|
throw error;
|
|
321
420
|
}
|
|
322
|
-
}
|
|
421
|
+
}
|
|
422
|
+
return { handle: routeMcp, rejectOrigin };
|
|
323
423
|
}
|
|
@@ -14,6 +14,7 @@ async function handleOAuthManagementRequest(context, connectorId) {
|
|
|
14
14
|
validateAuthPermissions(authz, opts.registry);
|
|
15
15
|
registry = opts.registry.scoped({
|
|
16
16
|
connectorIds: authz.connectorIds,
|
|
17
|
+
...(authz.toolAccess ? { toolAccess: authz.toolAccess } : {}),
|
|
17
18
|
...(authz.subjectKey ? { subjectKey: authz.subjectKey } : {}),
|
|
18
19
|
...(authz.principalKey ? { principalKey: authz.principalKey } : {}),
|
|
19
20
|
});
|
package/dist/routes/oauth.js
CHANGED
|
@@ -137,6 +137,10 @@ export async function routeOAuthCallback(context) {
|
|
|
137
137
|
return refused();
|
|
138
138
|
}
|
|
139
139
|
const expectedPrincipalKey = callbackTarget?.principalKey;
|
|
140
|
+
// A browser returning from consent normally has no MCP Authorization
|
|
141
|
+
// header. An interactive bearer provider therefore answers 401 here; state
|
|
142
|
+
// and the saved state-to-principal handoff still prove ownership below.
|
|
143
|
+
// Rejecting 401 would break that callback. A 403 is an explicit denial.
|
|
140
144
|
const browserIdentity = await authorizeUiIdentity(context.request, baseUrl, opts.auth, "OAuth callback", context.runtimeContext, opts.identity);
|
|
141
145
|
if (browserIdentity.ok) {
|
|
142
146
|
try {
|
package/dist/routes/shared.d.ts
CHANGED
|
@@ -4,7 +4,8 @@ import type { ActivityActor, ActivityReadGate, ActivityStore } from "../activity
|
|
|
4
4
|
import type { CredentialVault } from "../credential-contract.js";
|
|
5
5
|
import type { DeferredWork } from "../connector-scope.js";
|
|
6
6
|
import type { AdmissionController } from "../executor-admission.js";
|
|
7
|
-
import type { Registry } from "../registry.js";
|
|
7
|
+
import type { Registry, ToolAccess } from "../registry.js";
|
|
8
|
+
import type { ResolvedPool } from "../connector-access.js";
|
|
8
9
|
import type { AuthenticatedIdentity, ConnectaBranding, Executor, InboundAuth, InboundAuthRuntimeContext, Logger } from "../types.js";
|
|
9
10
|
import type { ConnectorPermission, ConnectaIdentityConfig } from "../index.js";
|
|
10
11
|
export { msg } from "../errors.js";
|
|
@@ -12,7 +13,10 @@ export interface ServerOptions {
|
|
|
12
13
|
registry: Registry;
|
|
13
14
|
auth: InboundAuth[];
|
|
14
15
|
identity?: ConnectaIdentityConfig | undefined;
|
|
16
|
+
/** Validated named pools served at `/mcp/<name>`; empty when none declared. */
|
|
17
|
+
pools?: ReadonlyMap<string, ResolvedPool> | undefined;
|
|
15
18
|
publicUrl?: string | undefined;
|
|
19
|
+
allowedOrigins?: readonly string[] | "*" | undefined;
|
|
16
20
|
serverInfo: Implementation;
|
|
17
21
|
logger: Logger;
|
|
18
22
|
activity?: ActivityStore | undefined;
|
|
@@ -78,6 +82,8 @@ export declare function authorize(request: Request, baseUrl: string, auth: Inbou
|
|
|
78
82
|
subjectKey?: string;
|
|
79
83
|
principalKey?: string;
|
|
80
84
|
connectorIds: "all" | readonly string[];
|
|
85
|
+
/** Per-connector tool allowlist for connectors granted by address only. */
|
|
86
|
+
toolAccess?: ToolAccess;
|
|
81
87
|
operator: boolean;
|
|
82
88
|
credentialAdministration: ConnectorPermission;
|
|
83
89
|
personalConnection: ConnectorPermission;
|
package/dist/routes/shared.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseConnectorAccess } from "../connector-access.js";
|
|
1
2
|
import { identityStorageKey, validIdentityReference } from "../identity.js";
|
|
2
3
|
export { msg } from "../errors.js";
|
|
3
4
|
export function privateJson(body, init = {}) {
|
|
@@ -33,11 +34,9 @@ export async function authorize(request, baseUrl, auth, runtimeContext, identity
|
|
|
33
34
|
if (auth.length === 0) {
|
|
34
35
|
const actor = { kind: "anonymous" };
|
|
35
36
|
const identity = { actor, interactive: false };
|
|
36
|
-
let
|
|
37
|
+
let access;
|
|
37
38
|
try {
|
|
38
|
-
|
|
39
|
-
if (connectorIds !== "all" && (!Array.isArray(connectorIds) || !connectorIds.every(id => typeof id === "string" && /^[a-z0-9_-]+$/.test(id))))
|
|
40
|
-
throw new Error("invalid connector permission");
|
|
39
|
+
access = parseConnectorAccess(identityConfig?.connectorAccess ? await identityConfig.connectorAccess(identity) : "all");
|
|
41
40
|
}
|
|
42
41
|
catch {
|
|
43
42
|
return {
|
|
@@ -45,7 +44,7 @@ export async function authorize(request, baseUrl, auth, runtimeContext, identity
|
|
|
45
44
|
response: privateJson({ error: "identity access resolution failed" }, { status: 403 }),
|
|
46
45
|
};
|
|
47
46
|
}
|
|
48
|
-
return { ok: true, actor, identity,
|
|
47
|
+
return { ok: true, actor, identity, ...access, operator: false, credentialAdministration: "none", personalConnection: "none" };
|
|
49
48
|
}
|
|
50
49
|
let lastResponse = null;
|
|
51
50
|
for (const provider of auth) {
|
|
@@ -53,20 +52,20 @@ export async function authorize(request, baseUrl, auth, runtimeContext, identity
|
|
|
53
52
|
if (result.ok) {
|
|
54
53
|
const subjectId = result.subjectId ?? result.userId;
|
|
55
54
|
const actorNamespace = activityActorNamespace(provider);
|
|
56
|
-
const subject = subjectId && actorNamespace
|
|
57
|
-
? { namespace: actorNamespace, id: subjectId }
|
|
58
|
-
: undefined;
|
|
59
55
|
const derivedPrincipal = result.userId && actorNamespace
|
|
60
56
|
? { namespace: actorNamespace, id: result.userId }
|
|
61
57
|
: undefined;
|
|
62
58
|
const principal = validIdentityReference(result.principal)
|
|
63
59
|
? result.principal
|
|
64
60
|
: derivedPrincipal;
|
|
61
|
+
const subject = subjectId
|
|
62
|
+
? { namespace: actorNamespace ?? `connecta:auth:${provider.kind}`, id: subjectId }
|
|
63
|
+
: principal;
|
|
65
64
|
const interactive = Boolean(result.userId && provider.interactiveOperator);
|
|
66
65
|
const actor = {
|
|
67
66
|
kind: provider.kind,
|
|
68
67
|
...(subjectId ? { id: subjectId } : {}),
|
|
69
|
-
...(
|
|
68
|
+
...(subjectId && actorNamespace ? { namespace: actorNamespace } : {}),
|
|
70
69
|
};
|
|
71
70
|
const identity = {
|
|
72
71
|
actor,
|
|
@@ -77,21 +76,21 @@ export async function authorize(request, baseUrl, auth, runtimeContext, identity
|
|
|
77
76
|
let operator = interactive;
|
|
78
77
|
let credentialAdministration = "none";
|
|
79
78
|
let personalConnection = "none";
|
|
80
|
-
let
|
|
79
|
+
let access;
|
|
81
80
|
try {
|
|
82
81
|
if (identityConfig?.activityAccess) {
|
|
83
82
|
operator = interactive && principal
|
|
84
83
|
? await identityConfig.activityAccess(principal)
|
|
85
84
|
: false;
|
|
86
85
|
}
|
|
87
|
-
|
|
86
|
+
access = parseConnectorAccess(identityConfig?.connectorAccess ? await identityConfig.connectorAccess(identity) : "all");
|
|
88
87
|
if (interactive) {
|
|
89
88
|
credentialAdministration = identityConfig?.credentialAdministration ? await identityConfig.credentialAdministration(identity) : "none";
|
|
90
89
|
personalConnection = principal && identityConfig?.personalConnection ? await identityConfig.personalConnection(identity) : "none";
|
|
91
90
|
}
|
|
92
91
|
if (typeof operator !== "boolean")
|
|
93
92
|
throw new Error("invalid activity permission");
|
|
94
|
-
for (const permission of [
|
|
93
|
+
for (const permission of [credentialAdministration, personalConnection]) {
|
|
95
94
|
if (permission !== "all" && permission !== "none" && (!Array.isArray(permission) || !permission.every(id => typeof id === "string" && /^[a-z0-9_-]+$/.test(id))))
|
|
96
95
|
throw new Error("invalid identity permission");
|
|
97
96
|
}
|
|
@@ -112,7 +111,7 @@ export async function authorize(request, baseUrl, auth, runtimeContext, identity
|
|
|
112
111
|
...(principal && partitionIdentity
|
|
113
112
|
? { principalKey: await identityStorageKey(principal) }
|
|
114
113
|
: {}),
|
|
115
|
-
|
|
114
|
+
...access,
|
|
116
115
|
credentialAdministration,
|
|
117
116
|
personalConnection,
|
|
118
117
|
operator,
|
package/dist/routes/ui.js
CHANGED
|
@@ -111,6 +111,7 @@ export async function routeUi(context) {
|
|
|
111
111
|
validateAuthPermissions(authz, opts.registry);
|
|
112
112
|
registry = opts.registry.scoped({
|
|
113
113
|
connectorIds: authz.connectorIds,
|
|
114
|
+
...(authz.toolAccess ? { toolAccess: authz.toolAccess } : {}),
|
|
114
115
|
...(authz.subjectKey ? { subjectKey: authz.subjectKey } : {}),
|
|
115
116
|
...(authz.principalKey ? { principalKey: authz.principalKey } : {}),
|
|
116
117
|
});
|
|
@@ -133,7 +134,7 @@ export async function routeUi(context) {
|
|
|
133
134
|
const connector = registry.getConnector(detail[1]);
|
|
134
135
|
if (!connector)
|
|
135
136
|
return privateJson({ error: "unknown connector" }, { status: 404 });
|
|
136
|
-
const one = opts.registry.scoped({ connectorIds: [connector.id], ...(authz.subjectKey ? { subjectKey: authz.subjectKey } : {}), ...(authz.principalKey ? { principalKey: authz.principalKey } : {}) });
|
|
137
|
+
const one = opts.registry.scoped({ connectorIds: [connector.id], ...(authz.toolAccess ? { toolAccess: authz.toolAccess } : {}), ...(authz.subjectKey ? { subjectKey: authz.subjectKey } : {}), ...(authz.principalKey ? { principalKey: authz.principalKey } : {}) });
|
|
137
138
|
const data = await buildUiData(one, baseUrl, opts.serverInfo, opts.credentialVault, activityEnabled, credentialManagement, defer, false, 1, authz.principalKey, { mayManage, timeoutMs: opts.probeTimeoutMs ?? 30_000, signal: request.signal });
|
|
138
139
|
return privateJson({ ...data.connectors[0], permissions: permissions(connector) });
|
|
139
140
|
}
|
package/dist/server.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { aggregateCallAdmissionSnapshots } from "./call-admission.js";
|
|
1
2
|
import { isAdmittingExecutor } from "./executor-admission.js";
|
|
2
3
|
import { createMcpRoute, MCP_CORS_HEADERS } from "./routes/mcp.js";
|
|
3
4
|
import { routeOAuthCallback, } from "./routes/oauth.js";
|
|
@@ -18,6 +19,9 @@ export function createFetchHandler(opts) {
|
|
|
18
19
|
const defer = runtimeContext
|
|
19
20
|
? runtimeContext.waitUntil.bind(runtimeContext)
|
|
20
21
|
: undefined;
|
|
22
|
+
const originRefusal = routeMcp.rejectOrigin(request);
|
|
23
|
+
if (originRefusal)
|
|
24
|
+
return withSecurityHeaders(originRefusal, url, path);
|
|
21
25
|
// Container and orchestrator probes reach /health over plain HTTP on
|
|
22
26
|
// loopback, where no proxy has set X-Forwarded-Proto. Redirecting them to
|
|
23
27
|
// the public origin would make an internal liveness check depend on
|
|
@@ -59,6 +63,9 @@ export function createFetchHandler(opts) {
|
|
|
59
63
|
if (uiResponse)
|
|
60
64
|
return uiResponse;
|
|
61
65
|
if (request.method === "OPTIONS") {
|
|
66
|
+
const preflight = await routeMcp.handle(context);
|
|
67
|
+
if (preflight)
|
|
68
|
+
return preflight;
|
|
62
69
|
for (const provider of auth) {
|
|
63
70
|
if (provider.handleMetadata) {
|
|
64
71
|
const response = await provider.handleMetadata(request, baseUrl);
|
|
@@ -102,14 +109,19 @@ export function createFetchHandler(opts) {
|
|
|
102
109
|
// Counts only, from refreshes that already happened — the endpoint
|
|
103
110
|
// asks no downstream anything, and `connecta doctor` reads it to
|
|
104
111
|
// report a stale allowlist without a probe of its own (#343).
|
|
105
|
-
|
|
112
|
+
// Stable 64-bit hashes preserve that shape without publishing ids.
|
|
113
|
+
catalogDrift: Object.fromEntries(await Promise.all(Object.entries(registry.catalogDriftSnapshot()).map(async ([id, report]) => {
|
|
114
|
+
const hash = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(id)));
|
|
115
|
+
const key = Array.from(hash.subarray(0, 8), byte => byte.toString(16).padStart(2, "0")).join("");
|
|
116
|
+
return [key, report];
|
|
117
|
+
}))),
|
|
106
118
|
admission: {
|
|
107
119
|
policy: "global-fifo",
|
|
108
120
|
requests: opts.requestAdmission.snapshot(),
|
|
109
121
|
code: codeAdmission ?? { managedByExecutor: true },
|
|
110
122
|
downstreamCalls: {
|
|
111
123
|
policy: "connector-partitioned-per-runtime",
|
|
112
|
-
|
|
124
|
+
aggregate: aggregateCallAdmissionSnapshots(Object.values(registry.callAdmissionSnapshot())),
|
|
113
125
|
},
|
|
114
126
|
reservedRoutes: [
|
|
115
127
|
"/health",
|
|
@@ -123,7 +135,7 @@ export function createFetchHandler(opts) {
|
|
|
123
135
|
const oauthCallback = await routeOAuthCallback(context);
|
|
124
136
|
if (oauthCallback)
|
|
125
137
|
return oauthCallback;
|
|
126
|
-
const mcp = await routeMcp(context);
|
|
138
|
+
const mcp = await routeMcp.handle(context);
|
|
127
139
|
if (mcp)
|
|
128
140
|
return mcp;
|
|
129
141
|
return new Response("Not Found", { status: 404 });
|
package/dist/skills.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { boundedEchoText } from "./errors.js";
|
|
1
2
|
export const CONNECTA_INSTRUCTIONS = 'Choose a route before discovery. A known-address read needs only call_tool. Unknown-address read-only work starts with execute_code to discover, call, and return the answer; use the same route for reduction, multiple or dependent calls, loops, joins, or branches. Keep discovery and calls together when schemas suffice; do not return catalog matches alone. Inspect unfamiliar result shapes with a small sample before proceeding. Only readOnlyHint: true tools run there. Keep catalog inspection and unannotated, write-capable, or destructive work top level: search_tools then call_destructive_tool when a call is needed. After auth_required use authorize_connector. After a truncated direct result use get_result. Guidance is on demand: fetch skills({ name: "usage" }) only when these instructions and the tool description are insufficient or a run needs repair.';
|
|
2
3
|
const USAGE_SKILL_BASE = `# Connecta usage
|
|
3
4
|
|
|
@@ -423,14 +424,14 @@ export function resolveSkill(name, connectors) {
|
|
|
423
424
|
if (!connector) {
|
|
424
425
|
return {
|
|
425
426
|
found: false,
|
|
426
|
-
message: `Unknown connector "${id}". Available skills: ${available()}.`,
|
|
427
|
+
message: `Unknown connector "${boundedEchoText(id)}". Available skills: ${available()}.`,
|
|
427
428
|
};
|
|
428
429
|
}
|
|
429
430
|
const guide = connectorGuide(connector);
|
|
430
431
|
if (!guide) {
|
|
431
432
|
return {
|
|
432
433
|
found: false,
|
|
433
|
-
message: `Connector "${id}" has no usage guide. Available skills: ${available()}.`,
|
|
434
|
+
message: `Connector "${boundedEchoText(id)}" has no usage guide. Available skills: ${available()}.`,
|
|
434
435
|
};
|
|
435
436
|
}
|
|
436
437
|
return { found: true, content: guide };
|
|
@@ -440,12 +441,12 @@ export function resolveSkill(name, connectors) {
|
|
|
440
441
|
return {
|
|
441
442
|
found: false,
|
|
442
443
|
message: connectorGuide(bare)
|
|
443
|
-
? `Unknown skill "${name}". Connector guides are fetched as "${connectorSkillName(name)}". Available skills: ${available()}.`
|
|
444
|
-
: `Connector "${name}" has no usage guide. Available skills: ${available()}.`,
|
|
444
|
+
? `Unknown skill "${boundedEchoText(name)}". Connector guides are fetched as "${boundedEchoText(connectorSkillName(name))}". Available skills: ${available()}.`
|
|
445
|
+
: `Connector "${boundedEchoText(name)}" has no usage guide. Available skills: ${available()}.`,
|
|
445
446
|
};
|
|
446
447
|
}
|
|
447
448
|
return {
|
|
448
449
|
found: false,
|
|
449
|
-
message: `Unknown skill "${name}". Available skills: ${available()}.`,
|
|
450
|
+
message: `Unknown skill "${boundedEchoText(name)}". Available skills: ${available()}.`,
|
|
450
451
|
};
|
|
451
452
|
}
|
package/dist/storage/file.d.ts
CHANGED
|
@@ -5,7 +5,11 @@ export interface FileStorageOptions {
|
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
7
7
|
* JSON-file-backed KVStorage for Node. Loads once, persists on every write via
|
|
8
|
-
*
|
|
8
|
+
* an exclusive temp-file + rename. Refuses a second holder of the same path.
|
|
9
|
+
* Call close() when finished to release its lock; process exit also releases it.
|
|
10
|
+
* Only reachable via the "@zackbart/connecta/node"
|
|
9
11
|
* subpath so the main entry stays Workers-clean.
|
|
10
12
|
*/
|
|
11
|
-
export declare function fileStorage(path: string, opts?: FileStorageOptions): KVStorage
|
|
13
|
+
export declare function fileStorage(path: string, opts?: FileStorageOptions): KVStorage & {
|
|
14
|
+
close(): void;
|
|
15
|
+
};
|