@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.
Files changed (65) hide show
  1. package/CHANGELOG.md +169 -0
  2. package/dist/auth/bearer.js +2 -0
  3. package/dist/auth/clerk.d.ts +0 -5
  4. package/dist/auth/clerk.js +21 -8
  5. package/dist/auth/downstream-oauth.d.ts +12 -1
  6. package/dist/auth/downstream-oauth.js +147 -35
  7. package/dist/call-admission.d.ts +4 -0
  8. package/dist/call-admission.js +26 -0
  9. package/dist/catalog-drift.js +9 -4
  10. package/dist/catalog-service.d.ts +2 -0
  11. package/dist/catalog-service.js +25 -8
  12. package/dist/catalog.d.ts +2 -0
  13. package/dist/catalog.js +246 -121
  14. package/dist/connector-access.d.ts +32 -0
  15. package/dist/connector-access.js +79 -0
  16. package/dist/connectors/api.js +11 -1
  17. package/dist/connectors/guarded-fetch.d.ts +1 -1
  18. package/dist/connectors/guarded-fetch.js +27 -20
  19. package/dist/connectors/remote-mcp.js +84 -53
  20. package/dist/errors.d.ts +17 -0
  21. package/dist/errors.js +58 -0
  22. package/dist/execute.js +85 -23
  23. package/dist/executor-result.js +3 -1
  24. package/dist/executors/quickjs-child.js +5 -1
  25. package/dist/executors/quickjs-protocol.d.ts +4 -0
  26. package/dist/executors/quickjs-runtime.d.ts +1 -1
  27. package/dist/executors/quickjs-runtime.js +38 -21
  28. package/dist/executors/quickjs.js +68 -27
  29. package/dist/index.d.ts +37 -1
  30. package/dist/index.js +89 -3
  31. package/dist/invocation.js +134 -93
  32. package/dist/mcp-result.js +3 -2
  33. package/dist/meta-tools.js +118 -39
  34. package/dist/registry.d.ts +29 -1
  35. package/dist/registry.js +122 -15
  36. package/dist/routes/credentials.js +1 -0
  37. package/dist/routes/mcp.d.ts +4 -1
  38. package/dist/routes/mcp.js +112 -12
  39. package/dist/routes/oauth-management.js +1 -0
  40. package/dist/routes/oauth.js +4 -0
  41. package/dist/routes/shared.d.ts +7 -1
  42. package/dist/routes/shared.js +12 -13
  43. package/dist/routes/ui.js +2 -1
  44. package/dist/server.js +15 -3
  45. package/dist/skills.js +6 -5
  46. package/dist/storage/file.d.ts +6 -2
  47. package/dist/storage/file.js +312 -34
  48. package/dist/storage/memory.js +12 -1
  49. package/dist/validate.js +3 -3
  50. package/dist/version.d.ts +1 -1
  51. package/dist/version.js +1 -1
  52. package/documentation/architecture.md +30 -9
  53. package/documentation/auth.md +110 -6
  54. package/documentation/call-admission.md +24 -8
  55. package/documentation/code-mode.md +34 -22
  56. package/documentation/connectors.md +47 -5
  57. package/documentation/meta-tools.md +74 -6
  58. package/documentation/operations.md +20 -19
  59. package/documentation/provider-conventions.md +7 -0
  60. package/documentation/request-admission.md +38 -4
  61. package/documentation/storage-and-credentials.md +54 -1
  62. package/documentation/upgrading.md +21 -5
  63. package/ethos.md +1 -1
  64. package/package.json +1 -1
  65. package/templates/node/package.json +1 -1
@@ -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
- code: overloaded ? -32001 : -32002,
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
- return async function routeMcp(context) {
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 withMcpCors(requestAdmissionFailure(error));
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(withMcpCors(authz.response), admission, request.signal);
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: authz.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(withMcpCors(new Response(JSON.stringify({ error: msg(error) }), {
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(withMcpCors(toolkitRetired(opts.logger)), admission, request.signal);
413
+ return releaseAdmissionWithResponse(cors(toolkitRetired(opts.logger)), admission, request.signal);
315
414
  }
316
- return releaseAdmissionWithResponse(withMcpCors(await serveMcp(request, opts, baseUrl, authz.actor, scopedRegistry, id => { const connector = scopedRegistry.getConnector(id); return Boolean(connector && mayManageConnector(authz, connector)); }, runtimeContext)), admission, request.signal);
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
  });
@@ -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 {
@@ -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;
@@ -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 connectorIds = "all";
37
+ let access;
37
38
  try {
38
- connectorIds = identityConfig?.connectorAccess ? await identityConfig.connectorAccess(identity) : "all";
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, connectorIds, operator: false, credentialAdministration: "none", personalConnection: "none" };
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
- ...(subject ? { namespace: subject.namespace } : {}),
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 connectorIds = "all";
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
- connectorIds = identityConfig?.connectorAccess ? await identityConfig.connectorAccess(identity) : "all";
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 [connectorIds, credentialAdministration, personalConnection]) {
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
- connectorIds,
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
- catalogDrift: registry.catalogDriftSnapshot(),
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
- connectors: registry.callAdmissionSnapshot(),
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
  }
@@ -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
- * a temp-file + rename (atomic-ish). Only reachable via the "@zackbart/connecta/node"
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
+ };