@zackbart/connecta 0.24.2 → 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 +141 -0
- package/dist/auth/bearer.js +2 -0
- 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/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 +14 -0
- package/dist/index.js +24 -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 +14 -2
- package/dist/registry.js +87 -13
- package/dist/routes/mcp.d.ts +4 -1
- package/dist/routes/mcp.js +84 -13
- package/dist/routes/oauth.js +4 -0
- package/dist/routes/shared.d.ts +1 -0
- package/dist/routes/shared.js +4 -4
- 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 +22 -6
- package/documentation/auth.md +42 -9
- 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 +19 -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 +18 -4
- package/package.json +1 -1
- package/templates/node/package.json +1 -1
package/dist/registry.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { closeConnectorScope, } from "./connector-scope.js";
|
|
2
2
|
import { storedCredentialShape, } from "./credential-rules.js";
|
|
3
3
|
import { ConnectorCallError, msg } from "./errors.js";
|
|
4
|
-
import { ConnectorCallAdmissionController, } from "./call-admission.js";
|
|
4
|
+
import { ConnectorCallAdmissionController, aggregateCallAdmissionSnapshots, } from "./call-admission.js";
|
|
5
5
|
import { boundedCatalogDrift } from "./catalog-drift.js";
|
|
6
6
|
import { fingerprintSerializedCatalog, snapshotCatalog, } from "./catalog-fingerprint.js";
|
|
7
7
|
import { MAX_CATALOG_CHUNK_BYTES, MAX_CATALOG_TOOLS, MAX_SERIALIZED_CATALOG_BYTES, } from "./catalog-limits.js";
|
|
@@ -80,6 +80,7 @@ function namespaced(storage, prefix) {
|
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
82
|
const MAX_PERSONAL_REGISTRIES = 1_024;
|
|
83
|
+
const MAX_ABSENT_GRANT_WARNINGS = 1_024;
|
|
83
84
|
const OAUTH_HANDOFF_TTL_SECONDS = 15 * 60;
|
|
84
85
|
async function sha256Hex(value) {
|
|
85
86
|
const bytes = new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(value)));
|
|
@@ -117,9 +118,13 @@ export class Registry {
|
|
|
117
118
|
persistToolCatalog;
|
|
118
119
|
/** Result-size guard cap threaded to the meta-tools. */
|
|
119
120
|
maxResultBytes;
|
|
121
|
+
/** Only keys, byte counts, and expiry survive requests; never write promises. */
|
|
122
|
+
resultStash = new Map();
|
|
123
|
+
resultStashBytes = 0;
|
|
120
124
|
configuredConnectors;
|
|
121
125
|
personalRegistries = new Map();
|
|
122
|
-
|
|
126
|
+
callAdmissionClosed = false;
|
|
127
|
+
/** Bounded FIFO of absent grants already warned about. */
|
|
123
128
|
warnedAbsentGrants = new Set();
|
|
124
129
|
constructor(connectors, opts) {
|
|
125
130
|
this.opts = opts;
|
|
@@ -174,18 +179,24 @@ export class Registry {
|
|
|
174
179
|
this.personalRegistries.set(principalKey, existing);
|
|
175
180
|
return existing;
|
|
176
181
|
}
|
|
182
|
+
if (this.personalRegistries.size >= MAX_PERSONAL_REGISTRIES) {
|
|
183
|
+
// Eviction must not reset a live rolling budget or orphan queued calls.
|
|
184
|
+
const idle = [...this.personalRegistries].find(([, candidate]) => [...candidate.callAdmission.values()].every(admission => admission.isIdle()));
|
|
185
|
+
if (!idle) {
|
|
186
|
+
throw new Error("Personal connector capacity is exhausted; retry after calls and rolling budgets drain.");
|
|
187
|
+
}
|
|
188
|
+
idle[1].closeCallAdmission();
|
|
189
|
+
this.personalRegistries.delete(idle[0]);
|
|
190
|
+
}
|
|
177
191
|
const registry = new Registry(this.configuredConnectors.filter((connector) => connector.authScope === "personal"), {
|
|
178
192
|
...this.opts,
|
|
179
193
|
storage: namespaced(this.opts.storage, `principal:${principalKey}:`),
|
|
180
194
|
credentialOwner: principalKey,
|
|
181
195
|
constructionChecks: false,
|
|
182
196
|
});
|
|
197
|
+
if (this.callAdmissionClosed)
|
|
198
|
+
registry.closeCallAdmission();
|
|
183
199
|
this.personalRegistries.set(principalKey, registry);
|
|
184
|
-
const oldest = this.personalRegistries.keys().next().value;
|
|
185
|
-
if (this.personalRegistries.size > MAX_PERSONAL_REGISTRIES &&
|
|
186
|
-
typeof oldest === "string") {
|
|
187
|
-
this.personalRegistries.delete(oldest);
|
|
188
|
-
}
|
|
189
200
|
return registry;
|
|
190
201
|
}
|
|
191
202
|
/** Build the only connector view an authenticated request receives. */
|
|
@@ -214,6 +225,11 @@ export class Registry {
|
|
|
214
225
|
if (this.warnedAbsentGrants.has(key))
|
|
215
226
|
return;
|
|
216
227
|
this.warnedAbsentGrants.add(key);
|
|
228
|
+
if (this.warnedAbsentGrants.size > MAX_ABSENT_GRANT_WARNINGS) {
|
|
229
|
+
const oldest = this.warnedAbsentGrants.values().next().value;
|
|
230
|
+
if (oldest !== undefined)
|
|
231
|
+
this.warnedAbsentGrants.delete(oldest);
|
|
232
|
+
}
|
|
217
233
|
// Grant names are operator data but may carry any non-control character;
|
|
218
234
|
// quote them so a line terminator a log reader honours cannot forge a line.
|
|
219
235
|
const quoted = JSON.stringify(key).replace(/[\u2028\u2029]/g, (ch) => `\\u${ch.charCodeAt(0).toString(16)}`);
|
|
@@ -345,11 +361,19 @@ export class Registry {
|
|
|
345
361
|
return admission.acquire(input);
|
|
346
362
|
return Promise.resolve({ waitMs: 0, release() { } });
|
|
347
363
|
}
|
|
348
|
-
/**
|
|
364
|
+
/** Connector totals across root and personal controllers; health removes ids. */
|
|
349
365
|
callAdmissionSnapshot() {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
admission.snapshot()
|
|
366
|
+
const snapshots = new Map();
|
|
367
|
+
for (const [id, admission] of this.callAdmission) {
|
|
368
|
+
snapshots.set(id, [admission.snapshot()]);
|
|
369
|
+
}
|
|
370
|
+
for (const registry of this.personalRegistries.values()) {
|
|
371
|
+
for (const [id, admission] of registry.callAdmission) {
|
|
372
|
+
snapshots.get(id)?.push(admission.snapshot());
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return Object.fromEntries([...snapshots].map(([id, values]) => [
|
|
376
|
+
id, aggregateCallAdmissionSnapshots(values),
|
|
353
377
|
]));
|
|
354
378
|
}
|
|
355
379
|
/**
|
|
@@ -416,8 +440,54 @@ export class Registry {
|
|
|
416
440
|
}
|
|
417
441
|
/** Reject queued/future downstream admission; active permits release safely. */
|
|
418
442
|
closeCallAdmission() {
|
|
443
|
+
this.callAdmissionClosed = true;
|
|
419
444
|
for (const admission of this.callAdmission.values())
|
|
420
445
|
admission.close();
|
|
446
|
+
for (const registry of this.personalRegistries.values())
|
|
447
|
+
registry.closeCallAdmission();
|
|
448
|
+
}
|
|
449
|
+
/** Reserve capacity and write one ASCII paging envelope in this runtime. */
|
|
450
|
+
async stashResult(key, value, ttlSeconds, prefix = "results:") {
|
|
451
|
+
const maxBytes = this.opts.results?.maxStashBytes ?? 8 * 1024 * 1024;
|
|
452
|
+
const maxEntries = this.opts.results?.maxStashEntries ?? 64;
|
|
453
|
+
// The paging envelope is ASCII, so its string length is its stored byte count.
|
|
454
|
+
const bytes = value.length;
|
|
455
|
+
if (bytes > maxBytes || maxEntries === 0)
|
|
456
|
+
return false;
|
|
457
|
+
const now = Date.now();
|
|
458
|
+
for (const [oldKey, entry] of this.resultStash) {
|
|
459
|
+
if (entry.busy || entry.expiresAt > now)
|
|
460
|
+
continue;
|
|
461
|
+
entry.busy = true;
|
|
462
|
+
try {
|
|
463
|
+
// TTL alone cannot reclaim a lazy backend. Keep the charge until deletion
|
|
464
|
+
// succeeds, including writes which persisted before throwing.
|
|
465
|
+
await this.opts.storage.delete(oldKey);
|
|
466
|
+
this.resultStash.delete(oldKey);
|
|
467
|
+
this.resultStashBytes -= entry.bytes;
|
|
468
|
+
}
|
|
469
|
+
finally {
|
|
470
|
+
entry.busy = false;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (this.resultStash.size >= maxEntries || this.resultStashBytes + bytes > maxBytes)
|
|
474
|
+
return false;
|
|
475
|
+
const fullKey = prefix + key;
|
|
476
|
+
const entry = { bytes, expiresAt: Infinity, busy: true };
|
|
477
|
+
this.resultStash.set(fullKey, entry);
|
|
478
|
+
this.resultStashBytes += bytes;
|
|
479
|
+
try {
|
|
480
|
+
await this.opts.storage.set(fullKey, value, { ttlSeconds });
|
|
481
|
+
entry.expiresAt = Date.now() + ttlSeconds * 1000;
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
catch (error) {
|
|
485
|
+
entry.expiresAt = 0;
|
|
486
|
+
throw error;
|
|
487
|
+
}
|
|
488
|
+
finally {
|
|
489
|
+
entry.busy = false;
|
|
490
|
+
}
|
|
421
491
|
}
|
|
422
492
|
/**
|
|
423
493
|
* Storage namespaced to the meta-tool result store (`results:` prefix), kept
|
|
@@ -1125,10 +1195,14 @@ class ScopedRegistryView {
|
|
|
1125
1195
|
return registry.contextFor(...args);
|
|
1126
1196
|
}
|
|
1127
1197
|
admitCall(...args) {
|
|
1128
|
-
|
|
1198
|
+
const registry = this.registryFor(args[0]);
|
|
1199
|
+
if (!registry) {
|
|
1129
1200
|
return Promise.reject(new Error(`Unknown connector "${args[0]}"`));
|
|
1130
1201
|
}
|
|
1131
|
-
return
|
|
1202
|
+
return registry.admitCall(...args);
|
|
1203
|
+
}
|
|
1204
|
+
stashResult(key, value, ttlSeconds) {
|
|
1205
|
+
return this.root.stashResult(key, value, ttlSeconds, this.scope.subjectKey ? `subject:${this.scope.subjectKey}:` : "results:");
|
|
1132
1206
|
}
|
|
1133
1207
|
resultsStorage() {
|
|
1134
1208
|
return this.scope.subjectKey
|
package/dist/routes/mcp.d.ts
CHANGED
|
@@ -4,4 +4,7 @@ export declare const MCP_CORS_HEADERS: {
|
|
|
4
4
|
"Access-Control-Allow-Methods": string;
|
|
5
5
|
"Access-Control-Allow-Headers": string;
|
|
6
6
|
};
|
|
7
|
-
export declare function createMcpRoute(opts: ServerOptions):
|
|
7
|
+
export declare function createMcpRoute(opts: ServerOptions): {
|
|
8
|
+
handle(context: RouteContext): Promise<Response | null>;
|
|
9
|
+
rejectOrigin(request: Request): Response | null;
|
|
10
|
+
};
|
package/dist/routes/mcp.js
CHANGED
|
@@ -14,11 +14,26 @@ export const MCP_CORS_HEADERS = {
|
|
|
14
14
|
// Browser-based MCP clients call /mcp cross-origin. Without CORS on every
|
|
15
15
|
// response — errors included — the browser hides the 401, the client cannot
|
|
16
16
|
// read WWW-Authenticate, and OAuth discovery silently never starts.
|
|
17
|
-
function withMcpCors(response) {
|
|
17
|
+
function withMcpCors(response, request, allowedOrigin) {
|
|
18
18
|
const headers = new Headers(response.headers);
|
|
19
19
|
for (const [name, value] of Object.entries(MCP_CORS_HEADERS)) {
|
|
20
20
|
headers.set(name, value);
|
|
21
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
|
+
}
|
|
22
37
|
headers.set("Access-Control-Expose-Headers", "WWW-Authenticate, Retry-After, mcp-session-id, mcp-protocol-version");
|
|
23
38
|
return new Response(response.body, {
|
|
24
39
|
status: response.status,
|
|
@@ -46,7 +61,10 @@ function requestAdmissionFailure(error) {
|
|
|
46
61
|
jsonrpc: "2.0",
|
|
47
62
|
id: null,
|
|
48
63
|
error: {
|
|
49
|
-
|
|
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,
|
|
50
68
|
message: overloaded
|
|
51
69
|
? "Server capacity is exhausted. Retry later."
|
|
52
70
|
: "Server is shutting down.",
|
|
@@ -244,6 +262,49 @@ async function serveMcp(request, opts, baseUrl, actor, registry, canManageAuth,
|
|
|
244
262
|
return transport.handleRequest(request);
|
|
245
263
|
}
|
|
246
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
|
+
};
|
|
247
308
|
let lastAdmissionWarningAt = 0;
|
|
248
309
|
let suppressedAdmissionWarnings = 0;
|
|
249
310
|
const warnAdmissionRejected = (error) => {
|
|
@@ -261,12 +322,21 @@ export function createMcpRoute(opts) {
|
|
|
261
322
|
lastAdmissionWarningAt = now;
|
|
262
323
|
suppressedAdmissionWarnings = 0;
|
|
263
324
|
};
|
|
264
|
-
|
|
325
|
+
async function routeMcp(context) {
|
|
265
326
|
const { path, request, baseUrl, runtimeContext, } = context;
|
|
266
|
-
|
|
267
|
-
if (path !== "/mcp" && !poolPath)
|
|
327
|
+
if (path !== "/mcp" && !path.startsWith("/mcp/"))
|
|
268
328
|
return null;
|
|
269
|
-
const poolName =
|
|
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 }));
|
|
270
340
|
let admission;
|
|
271
341
|
try {
|
|
272
342
|
admission = await opts.requestAdmission.acquire({
|
|
@@ -289,14 +359,14 @@ export function createMcpRoute(opts) {
|
|
|
289
359
|
if (error.code === "executor_overloaded") {
|
|
290
360
|
warnAdmissionRejected(error);
|
|
291
361
|
}
|
|
292
|
-
return
|
|
362
|
+
return cors(requestAdmissionFailure(error));
|
|
293
363
|
}
|
|
294
364
|
throw error;
|
|
295
365
|
}
|
|
296
366
|
try {
|
|
297
367
|
const authz = await authorize(request, baseUrl, opts.auth, runtimeContext, opts.identity);
|
|
298
368
|
if (!authz.ok) {
|
|
299
|
-
return releaseAdmissionWithResponse(
|
|
369
|
+
return releaseAdmissionWithResponse(cors(authz.response), admission, request.signal);
|
|
300
370
|
}
|
|
301
371
|
// A pool endpoint narrows the identity's own view and nothing else. An
|
|
302
372
|
// undeclared name, a grant that refuses, and a grant that throws are
|
|
@@ -319,7 +389,7 @@ export function createMcpRoute(opts) {
|
|
|
319
389
|
if (!pool || !granted) {
|
|
320
390
|
opts.logger.warn(`[connecta] refused /mcp/${poolName} with 404: pool ${reason}` +
|
|
321
391
|
(authz.actor.id ? ` for ${loggableValue(authz.actor.id)}` : ""));
|
|
322
|
-
return releaseAdmissionWithResponse(
|
|
392
|
+
return releaseAdmissionWithResponse(cors(new Response("Not Found", { status: 404 })), admission, request.signal);
|
|
323
393
|
}
|
|
324
394
|
access = intersectAccess(authz, pool.access);
|
|
325
395
|
}
|
|
@@ -334,19 +404,20 @@ export function createMcpRoute(opts) {
|
|
|
334
404
|
});
|
|
335
405
|
}
|
|
336
406
|
catch (error) {
|
|
337
|
-
return releaseAdmissionWithResponse(
|
|
407
|
+
return releaseAdmissionWithResponse(cors(new Response(JSON.stringify({ error: msg(error) }), {
|
|
338
408
|
status: 403,
|
|
339
409
|
headers: { "Content-Type": "application/json" },
|
|
340
410
|
})), admission, request.signal);
|
|
341
411
|
}
|
|
342
412
|
if (new URL(request.url).searchParams.has("toolkit")) {
|
|
343
|
-
return releaseAdmissionWithResponse(
|
|
413
|
+
return releaseAdmissionWithResponse(cors(toolkitRetired(opts.logger)), admission, request.signal);
|
|
344
414
|
}
|
|
345
|
-
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);
|
|
346
416
|
}
|
|
347
417
|
catch (error) {
|
|
348
418
|
admission.release();
|
|
349
419
|
throw error;
|
|
350
420
|
}
|
|
351
|
-
}
|
|
421
|
+
}
|
|
422
|
+
return { handle: routeMcp, rejectOrigin };
|
|
352
423
|
}
|
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
|
@@ -16,6 +16,7 @@ export interface ServerOptions {
|
|
|
16
16
|
/** Validated named pools served at `/mcp/<name>`; empty when none declared. */
|
|
17
17
|
pools?: ReadonlyMap<string, ResolvedPool> | undefined;
|
|
18
18
|
publicUrl?: string | undefined;
|
|
19
|
+
allowedOrigins?: readonly string[] | "*" | undefined;
|
|
19
20
|
serverInfo: Implementation;
|
|
20
21
|
logger: Logger;
|
|
21
22
|
activity?: ActivityStore | undefined;
|
package/dist/routes/shared.js
CHANGED
|
@@ -52,20 +52,20 @@ export async function authorize(request, baseUrl, auth, runtimeContext, identity
|
|
|
52
52
|
if (result.ok) {
|
|
53
53
|
const subjectId = result.subjectId ?? result.userId;
|
|
54
54
|
const actorNamespace = activityActorNamespace(provider);
|
|
55
|
-
const subject = subjectId && actorNamespace
|
|
56
|
-
? { namespace: actorNamespace, id: subjectId }
|
|
57
|
-
: undefined;
|
|
58
55
|
const derivedPrincipal = result.userId && actorNamespace
|
|
59
56
|
? { namespace: actorNamespace, id: result.userId }
|
|
60
57
|
: undefined;
|
|
61
58
|
const principal = validIdentityReference(result.principal)
|
|
62
59
|
? result.principal
|
|
63
60
|
: derivedPrincipal;
|
|
61
|
+
const subject = subjectId
|
|
62
|
+
? { namespace: actorNamespace ?? `connecta:auth:${provider.kind}`, id: subjectId }
|
|
63
|
+
: principal;
|
|
64
64
|
const interactive = Boolean(result.userId && provider.interactiveOperator);
|
|
65
65
|
const actor = {
|
|
66
66
|
kind: provider.kind,
|
|
67
67
|
...(subjectId ? { id: subjectId } : {}),
|
|
68
|
-
...(
|
|
68
|
+
...(subjectId && actorNamespace ? { namespace: actorNamespace } : {}),
|
|
69
69
|
};
|
|
70
70
|
const identity = {
|
|
71
71
|
actor,
|
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
|
+
};
|