@zackbart/connecta 0.28.0 → 0.29.0
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 +961 -5
- package/README.md +62 -39
- package/bin/connecta.mjs +110 -139
- package/bin/meta-tool-names.d.mts +2 -0
- package/bin/meta-tool-names.mjs +9 -0
- package/bin/version.mjs +3 -0
- package/dist/access-tokens.js +53 -63
- package/dist/activity-request.js +8 -0
- package/dist/activity.d.ts +76 -42
- package/dist/activity.js +110 -42
- package/dist/auth/clerk-sdk/client.d.ts +65 -0
- package/dist/auth/clerk-sdk/client.js +5 -0
- package/dist/auth/clerk-sdk/generated.js +1845 -0
- package/dist/auth/clerk-transport.js +19 -0
- package/dist/auth/clerk.d.ts +17 -7
- package/dist/auth/clerk.js +189 -107
- package/dist/auth/cloudflare-access.js +25 -26
- package/dist/auth/downstream-client-metadata.d.ts +29 -0
- package/dist/auth/downstream-client-metadata.js +133 -0
- package/dist/auth/downstream-oauth.js +1632 -1509
- package/dist/auth/downstream-revocation.js +106 -0
- package/dist/auth/oauth-token-response.js +231 -0
- package/dist/auth/oauth-v2-migration.js +116 -0
- package/dist/auth/static-oauth.js +526 -0
- package/dist/auth-elicitation.js +283 -0
- package/dist/branding.d.ts +8 -16
- package/dist/branding.js +26 -59
- package/dist/byte-read-response.js +39 -0
- package/dist/call-admission.js +12 -16
- package/dist/catalog-cache.js +739 -0
- package/dist/catalog-drift.d.ts +50 -15
- package/dist/catalog-drift.js +365 -128
- package/dist/catalog-fingerprint.js +2 -4
- package/dist/catalog-freshness.js +13 -0
- package/dist/catalog-limits.js +2 -2
- package/dist/catalog-service.js +368 -224
- package/dist/catalog.js +43 -84
- package/dist/concurrency.js +3 -4
- package/dist/config-defaults.d.ts +39 -0
- package/dist/config-defaults.js +46 -0
- package/dist/config-schema.d.ts +181 -0
- package/dist/config-schema.js +435 -0
- package/dist/config-value-sources.js +102 -0
- package/dist/config.d.ts +631 -0
- package/dist/config.js +503 -0
- package/dist/connector-access.d.ts +1 -0
- package/dist/connector-access.js +13 -15
- package/dist/connector-caller.d.ts +8 -0
- package/dist/connector-caller.js +15 -6
- package/dist/connector-scope.js +1 -1
- package/dist/connectors/api-connector.d.ts +220 -0
- package/dist/connectors/api-connector.js +214 -0
- package/dist/connectors/api.d.ts +8 -71
- package/dist/connectors/api.js +13 -99
- package/dist/connectors/guarded-fetch.d.ts +144 -0
- package/dist/connectors/guarded-fetch.js +108 -25
- package/dist/connectors/negotiation-cache.js +46 -0
- package/dist/connectors/option-shapes.js +27 -0
- package/dist/connectors/remote-mcp.d.ts +30 -4
- package/dist/connectors/remote-mcp.js +1780 -351
- package/dist/connectors/resource-uri.js +335 -0
- package/dist/credential-contract.d.ts +10 -0
- package/dist/credential-rules.js +1 -3
- package/dist/credential-url.js +108 -0
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +27 -18
- package/dist/d1.d.ts +130 -0
- package/dist/d1.js +290 -0
- package/dist/describe-config.d.ts +165 -0
- package/dist/describe-config.js +315 -0
- package/dist/described.js +106 -0
- package/dist/downstream-input-context.js +119 -0
- package/dist/downstream-input.js +323 -0
- package/dist/errors.d.ts +97 -20
- package/dist/errors.js +158 -80
- package/dist/execute.js +453 -305
- package/dist/executor-admission.d.ts +1 -0
- package/dist/executor-admission.js +11 -15
- package/dist/executor-contract.d.ts +20 -0
- package/dist/executor-contract.js +48 -0
- package/dist/executor-result.js +24 -9
- package/dist/executors/quickjs-child.js +3 -4
- package/dist/executors/quickjs-runtime.js +147 -59
- package/dist/executors/quickjs.js +29 -35
- package/dist/favicon.js +1 -2
- package/dist/fix-prompt.js +1 -3
- package/dist/guest-runtime.js +188 -0
- package/dist/guest-types.d.ts +176 -0
- package/dist/guest-types.js +1 -0
- package/dist/html-security.js +24 -0
- package/dist/identity.js +1 -3
- package/dist/inbound-credential.js +52 -0
- package/dist/index.d.ts +25 -257
- package/dist/index.js +65 -478
- package/dist/invocation-auth.js +69 -0
- package/dist/invocation.js +230 -73
- package/dist/json-schema.d.ts +1 -1
- package/dist/mcp-client-context.js +35 -0
- package/dist/mcp-result.js +11 -7
- package/dist/meta-output.js +76 -0
- package/dist/meta-tool-names.d.ts +2 -0
- package/dist/meta-tool-names.js +3 -0
- package/dist/meta-tools.js +285 -209
- package/dist/module-contracts.d.ts +8 -23
- package/dist/node.d.ts +0 -2
- package/dist/node.js +2 -2
- package/dist/oauth-callback-outcome.js +1 -3
- package/dist/oauth-handoff.js +117 -0
- package/dist/oauth-partition.js +17 -0
- package/dist/oauth-sealing.js +20 -0
- package/dist/operator-record.d.ts +137 -0
- package/dist/operator-record.js +420 -0
- package/dist/operator-ui/assets.js +31 -0
- package/dist/operator-ui/contract.d.ts +72 -0
- package/dist/operator-ui/contract.js +1 -0
- package/dist/operator-ui/fix-prompts.d.ts +18 -0
- package/dist/operator-ui/generated.js +5 -4
- package/dist/operator-ui/model.d.ts +12 -87
- package/dist/operator-ui/model.js +2 -9
- package/dist/operator-ui/setup-commands.js +1 -3
- package/dist/operator-ui/view.d.ts +323 -0
- package/dist/operator-ui/view.js +62 -189
- package/dist/page-styles.js +2 -3
- package/dist/program-source.js +14 -2
- package/dist/{exempt-writes.js → program-writes.js} +55 -18
- package/dist/provider.d.ts +142 -0
- package/dist/provider.js +187 -0
- package/dist/providers/_shared/google/result-size.js +57 -0
- package/dist/providers/_shared/google/service-account.d.ts +58 -0
- package/dist/providers/_shared/google/service-account.js +500 -0
- package/dist/providers/_shared/google/workspace.d.ts +276 -0
- package/dist/providers/_shared/google/workspace.js +518 -0
- package/dist/providers/basecamp/index.d.ts +43 -0
- package/dist/providers/basecamp/index.js +997 -0
- package/dist/providers/basecamp/skill.generated.js +9 -0
- package/dist/providers/breeze/index.d.ts +34 -0
- package/dist/providers/breeze/index.js +1446 -0
- package/dist/providers/breeze/skill.generated.js +9 -0
- package/dist/providers/ccb/index.d.ts +103 -0
- package/dist/providers/ccb/index.js +1725 -0
- package/dist/providers/ccb/skill.generated.js +9 -0
- package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
- package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
- package/dist/providers/cloudflare/skill.generated.js +10 -0
- package/dist/providers/docs/index.d.ts +54 -0
- package/dist/providers/docs/index.js +1237 -0
- package/dist/providers/docs/skill.generated.js +9 -0
- package/dist/providers/drive/index.d.ts +53 -0
- package/dist/providers/drive/index.js +1523 -0
- package/dist/providers/drive/skill.generated.js +9 -0
- package/dist/providers/forms/index.d.ts +54 -0
- package/dist/providers/forms/index.js +1217 -0
- package/dist/providers/forms/skill.generated.js +11 -0
- package/dist/providers/github/auth.d.ts +32 -0
- package/dist/providers/github/auth.js +361 -0
- package/dist/providers/github/hosted.js +218 -0
- package/dist/providers/github/index.d.ts +13 -0
- package/dist/providers/github/index.js +140 -0
- package/dist/providers/github/key.js +100 -0
- package/dist/providers/github/rest.js +321 -0
- package/dist/providers/github/scope.d.ts +29 -0
- package/dist/providers/github/scope.js +124 -0
- package/dist/providers/github/skill.generated.js +8 -0
- package/dist/providers/gmail/index.d.ts +51 -0
- package/dist/providers/gmail/index.js +1436 -0
- package/dist/providers/gmail/skill.generated.js +9 -0
- package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
- package/dist/providers/linear/index.js +352 -0
- package/dist/providers/linear/skill.generated.js +8 -0
- package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
- package/dist/providers/mixpanel/index.js +416 -0
- package/dist/providers/mixpanel/skill.generated.js +9 -0
- package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
- package/dist/providers/{notion.js → notion/index.js} +226 -239
- package/dist/providers/notion/skill.generated.js +10 -0
- package/dist/providers/overflow/index.d.ts +60 -0
- package/dist/providers/overflow/index.js +1535 -0
- package/dist/providers/overflow/skill.generated.js +8 -0
- package/dist/providers/planning-center/index.d.ts +59 -0
- package/dist/providers/planning-center/index.js +2199 -0
- package/dist/providers/planning-center/skill.generated.js +10 -0
- package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
- package/dist/providers/revenuecat/index.js +557 -0
- package/dist/providers/revenuecat/skill.generated.js +11 -0
- package/dist/providers/sheets/index.d.ts +54 -0
- package/dist/providers/sheets/index.js +1237 -0
- package/dist/providers/sheets/skill.generated.js +10 -0
- package/dist/providers/slides/index.d.ts +53 -0
- package/dist/providers/slides/index.js +2645 -0
- package/dist/providers/slides/skill.generated.js +9 -0
- package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
- package/dist/providers/stripe/index.js +266 -0
- package/dist/providers/stripe/skill.generated.js +11 -0
- package/dist/providers/tithely/index.d.ts +49 -0
- package/dist/providers/tithely/index.js +1003 -0
- package/dist/providers/tithely/skill.generated.js +8 -0
- package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
- package/dist/providers/vercel/index.js +1038 -0
- package/dist/providers/vercel/mcp-ownership.js +212 -0
- package/dist/providers/vercel/skill.generated.js +11 -0
- package/dist/registry.d.ts +108 -171
- package/dist/registry.js +513 -925
- package/dist/request-state.js +21 -0
- package/dist/resource-read.js +100 -0
- package/dist/result-shapes.js +17 -49
- package/dist/retired-toolkits.js +1 -1
- package/dist/routes/access-tokens.js +3 -6
- package/dist/routes/activity-context.js +21 -0
- package/dist/routes/activity-disclosure.js +40 -0
- package/dist/routes/activity.js +59 -21
- package/dist/routes/actor-labels.js +3 -50
- package/dist/routes/connect.js +221 -0
- package/dist/routes/credentials.js +15 -27
- package/dist/routes/mcp.js +356 -130
- package/dist/routes/oauth-client-metadata.js +15 -0
- package/dist/routes/oauth-management.js +45 -107
- package/dist/routes/oauth.js +136 -57
- package/dist/routes/operator.js +3 -3
- package/dist/routes/shared.d.ts +25 -64
- package/dist/routes/shared.js +160 -80
- package/dist/routes/skills.js +30 -0
- package/dist/routes/ui-config.js +225 -0
- package/dist/routes/ui-data.js +40 -35
- package/dist/routes/ui.js +66 -56
- package/dist/runtime/admission.js +4 -2
- package/dist/runtime/call-admission.js +3 -1
- package/dist/runtime/connector-scope.js +1 -4
- package/dist/runtime/run.js +10 -39
- package/dist/runtime/services.js +12 -117
- package/dist/runtime/shared-read.js +1 -3
- package/dist/runtime/storage.js +8 -1
- package/dist/sent-secrets.d.ts +52 -0
- package/dist/sent-secrets.js +474 -0
- package/dist/server.js +28 -71
- package/dist/skills.js +406 -174
- package/dist/sqlite.d.ts +48 -0
- package/dist/sqlite.js +166 -0
- package/dist/storage/keys.js +428 -0
- package/dist/storage/memory.js +7 -0
- package/dist/storage/sql.d.ts +66 -0
- package/dist/storage/sql.js +534 -0
- package/dist/timeout.js +4 -2
- package/dist/tool-name.js +17 -0
- package/dist/tool-safety.d.ts +9 -35
- package/dist/tool-safety.js +20 -40
- package/dist/types.d.ts +320 -54
- package/dist/types.js +0 -1
- package/dist/ui.d.ts +7 -19
- package/dist/ui.js +76 -90
- package/dist/url-safety.js +3 -6
- package/dist/usage-guide.js +137 -0
- package/dist/validate.d.ts +7 -0
- package/dist/validate.js +309 -47
- package/dist/version.d.ts +1 -7
- package/dist/version.js +3 -7
- package/dist/worker.d.ts +11 -0
- package/dist/worker.js +434 -0
- package/documentation/architecture.md +665 -230
- package/documentation/auth.md +962 -458
- package/documentation/code-mode.md +501 -312
- package/documentation/meta-tools.md +449 -200
- package/documentation/operator-tests.md +93 -0
- package/documentation/provider-migration-0.29.md +60 -0
- package/examples/worker/AGENTS.md +34 -12
- package/examples/worker/README.md +286 -255
- package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
- package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
- package/examples/worker/src/connecta.config.ts +151 -0
- package/examples/worker/src/index.ts +14 -197
- package/examples/worker/wrangler.jsonc +15 -35
- package/package.json +125 -35
- package/templates/node/.dockerignore +1 -2
- package/templates/node/.env.example +20 -21
- package/templates/node/AGENTS.md +21 -17
- package/templates/node/Dockerfile +3 -5
- package/templates/node/README.md +133 -58
- package/templates/node/docker-compose.yml +11 -12
- package/templates/node/package.json +4 -2
- package/templates/node/src/connecta.config.ts +119 -0
- package/templates/node/src/index.ts +9 -154
- package/templates/node/src/provision-token.ts +17 -0
- package/AGENTS.md +0 -170
- package/dist/artifacts/connector.d.ts +0 -58
- package/dist/artifacts/connector.js +0 -730
- package/dist/artifacts/document.js +0 -86
- package/dist/artifacts/guide.js +0 -140
- package/dist/artifacts/html-scan.js +0 -329
- package/dist/artifacts/json.js +0 -78
- package/dist/artifacts/kv-store.d.ts +0 -20
- package/dist/artifacts/kv-store.js +0 -140
- package/dist/artifacts/markdown.js +0 -478
- package/dist/artifacts/module.d.ts +0 -37
- package/dist/artifacts/module.js +0 -66
- package/dist/artifacts/operations.d.ts +0 -197
- package/dist/artifacts/operations.js +0 -1002
- package/dist/artifacts/refresh.d.ts +0 -55
- package/dist/artifacts/refresh.js +0 -236
- package/dist/artifacts/routes.js +0 -206
- package/dist/artifacts/types.d.ts +0 -214
- package/dist/artifacts/types.js +0 -22
- package/dist/artifacts/validate.d.ts +0 -62
- package/dist/artifacts/validate.js +0 -651
- package/dist/artifacts.d.ts +0 -6
- package/dist/artifacts.js +0 -9
- package/dist/auth/bearer.d.ts +0 -11
- package/dist/auth/bearer.js +0 -50
- package/dist/providers/linear.js +0 -217
- package/dist/providers/mixpanel.js +0 -247
- package/dist/providers/revenuecat.js +0 -317
- package/dist/providers/stripe.js +0 -253
- package/dist/providers/vercel.js +0 -1253
- package/dist/storage/file.d.ts +0 -15
- package/dist/storage/file.js +0 -455
- package/ethos.md +0 -93
- package/examples/worker/src/cloudflare-kv.ts +0 -43
- package/examples/worker/src/d1-activity-row.ts +0 -155
- package/examples/worker/src/d1-activity.ts +0 -147
- package/examples/worker/src/d1-storage.ts +0 -138
- package/examples/worker/src/r2-artifact-blobs.ts +0 -29
- package/templates/node/src/file-activity.ts +0 -182
package/dist/routes/shared.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { authorizationCredential, authorizationRequest, isMachineCredential } from "../inbound-credential.js";
|
|
2
|
+
import { htmlSecurityHeaders } from "../html-security.js";
|
|
1
3
|
import { parseConnectorAccess } from "../connector-access.js";
|
|
2
4
|
import { identityStorageKey, validIdentityReference } from "../identity.js";
|
|
3
|
-
import { isExplicitlyReadOnly } from "../tool-safety.js";
|
|
4
5
|
export { msg } from "../errors.js";
|
|
5
6
|
export function privateJson(body, init = {}) {
|
|
6
7
|
const headers = new Headers(init.headers);
|
|
@@ -31,8 +32,75 @@ export function activityActorNamespace(provider) {
|
|
|
31
32
|
? provider.activityActorNamespace
|
|
32
33
|
: undefined;
|
|
33
34
|
}
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
/** Metadata routing and challenge selection use the same first actual answer. */
|
|
36
|
+
export async function authMetadata(request, baseUrl, auth) {
|
|
37
|
+
for (const provider of auth) {
|
|
38
|
+
const response = await provider.handleMetadata?.(request, baseUrl);
|
|
39
|
+
if (response)
|
|
40
|
+
return { provider, response };
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
async function providerChallenge(response, request, baseUrl, auth, refusingProvider) {
|
|
45
|
+
if (response.status !== 401)
|
|
46
|
+
return response;
|
|
47
|
+
if (!response.headers.has("WWW-Authenticate")) {
|
|
48
|
+
const headers = new Headers(response.headers);
|
|
49
|
+
headers.set("WWW-Authenticate", refusingProvider?.challenge?.(request, baseUrl) ?? "Bearer");
|
|
50
|
+
response = new Response(response.body, { status: response.status, statusText: response.statusText, headers });
|
|
51
|
+
}
|
|
52
|
+
const path = new URL(request.url).pathname;
|
|
53
|
+
const pool = /^\/mcp\/([a-z0-9_-]+)$/.exec(path)?.[1];
|
|
54
|
+
const metadataRequest = new Request(new URL(`/.well-known/oauth-protected-resource${pool ? `/mcp/${pool}` : ""}`, baseUrl), { signal: request.signal });
|
|
55
|
+
const owner = await authMetadata(metadataRequest, baseUrl, auth);
|
|
56
|
+
if (!owner)
|
|
57
|
+
return response;
|
|
58
|
+
// Metadata is an ordinary bounded response, never retained across requests.
|
|
59
|
+
await owner.response.body?.cancel();
|
|
60
|
+
const challenge = owner.provider.challenge?.(request, baseUrl);
|
|
61
|
+
if (!challenge)
|
|
62
|
+
return response;
|
|
63
|
+
const headers = new Headers(response.headers);
|
|
64
|
+
headers.set("WWW-Authenticate", challenge);
|
|
65
|
+
return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
|
|
66
|
+
}
|
|
67
|
+
function recognizedProvider(request, auth, runtimeContext) {
|
|
68
|
+
for (const provider of auth) {
|
|
69
|
+
if (!provider.recognizesCredential)
|
|
70
|
+
continue;
|
|
71
|
+
const recognized = provider.recognizesCredential(request, runtimeContext);
|
|
72
|
+
if (typeof recognized !== "boolean") {
|
|
73
|
+
// A misimplemented hook may return a rejected Promise. Refuse without
|
|
74
|
+
// letting its arbitrary rejection reach an unhandled-rejection sink.
|
|
75
|
+
void Promise.resolve(recognized).catch(() => { });
|
|
76
|
+
throw new Error("invalid credential recognition verdict");
|
|
77
|
+
}
|
|
78
|
+
if (recognized)
|
|
79
|
+
return provider;
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
export async function authorize(request, baseUrl, auth, runtimeContext, identityConfig, partitionIdentity = true, interactiveOnly = false) {
|
|
84
|
+
const credential = authorizationCredential(request);
|
|
85
|
+
const explicit = credential.kind !== "absent";
|
|
86
|
+
if (credential.kind === "invalid") {
|
|
87
|
+
const challenge = auth.find((provider) => provider.kind !== "access_token")?.challenge?.(request, baseUrl) ?? "Bearer";
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
response: await providerChallenge(privateJson({ error: "unauthorized" }, {
|
|
91
|
+
status: 401,
|
|
92
|
+
headers: { "WWW-Authenticate": challenge },
|
|
93
|
+
}), request, baseUrl, auth),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
request = authorizationRequest(request);
|
|
97
|
+
if (explicit && runtimeContext)
|
|
98
|
+
runtimeContext = { waitUntil: runtimeContext.waitUntil.bind(runtimeContext) };
|
|
99
|
+
const machineCredential = isMachineCredential(request);
|
|
100
|
+
if (interactiveOnly && machineCredential) {
|
|
101
|
+
return { ok: false, response: privateJson({ error: "authenticated user required" }, { status: 403 }) };
|
|
102
|
+
}
|
|
103
|
+
if (auth.length === 0 && !interactiveOnly && !explicit) {
|
|
36
104
|
const actor = { kind: "anonymous" };
|
|
37
105
|
const identity = { actor, interactive: false };
|
|
38
106
|
let access;
|
|
@@ -45,28 +113,63 @@ export async function authorize(request, baseUrl, auth, runtimeContext, identity
|
|
|
45
113
|
response: privateJson({ error: "identity access resolution failed" }, { status: 403 }),
|
|
46
114
|
};
|
|
47
115
|
}
|
|
48
|
-
return {
|
|
116
|
+
return {
|
|
117
|
+
ok: true,
|
|
118
|
+
actor,
|
|
119
|
+
identity,
|
|
120
|
+
...access,
|
|
121
|
+
operator: false,
|
|
122
|
+
accessTokenManagement: false,
|
|
123
|
+
credentialAdministration: "none",
|
|
124
|
+
personalConnection: "none",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
let recognized;
|
|
128
|
+
try {
|
|
129
|
+
recognized = recognizedProvider(request, machineCredential ? auth.filter((provider) => provider.kind === "access_token") : auth, runtimeContext);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return { ok: false, response: privateJson({ error: "credential recognition failed" }, { status: 403 }) };
|
|
133
|
+
}
|
|
134
|
+
if (machineCredential && !recognized) {
|
|
135
|
+
return {
|
|
136
|
+
ok: false,
|
|
137
|
+
response: await providerChallenge(privateJson({ error: "unauthorized" }, {
|
|
138
|
+
status: 401,
|
|
139
|
+
headers: { "WWW-Authenticate": "Bearer" },
|
|
140
|
+
}), request, baseUrl, auth),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (interactiveOnly && recognized && !recognized.interactiveOperator) {
|
|
144
|
+
return { ok: false, response: privateJson({ error: "authenticated user required" }, { status: 403 }) };
|
|
49
145
|
}
|
|
50
146
|
let lastResponse = null;
|
|
51
|
-
|
|
147
|
+
const eligible = auth.filter((provider) => (!interactiveOnly || provider.interactiveOperator) &&
|
|
148
|
+
(!explicit || machineCredential || provider.kind !== "access_token" || !provider.recognizesCredential));
|
|
149
|
+
// A header owns one verdict even for adapters without a recognition hook.
|
|
150
|
+
const candidates = recognized ? [recognized] : explicit ? eligible.slice(0, 1) : eligible;
|
|
151
|
+
for (const provider of candidates) {
|
|
52
152
|
const result = await provider.authorize(request, baseUrl, runtimeContext);
|
|
53
153
|
if (result.ok) {
|
|
154
|
+
if (interactiveOnly && !result.userId) {
|
|
155
|
+
return { ok: false, response: privateJson({ error: "authenticated user required" }, { status: 403 }) };
|
|
156
|
+
}
|
|
54
157
|
const subjectId = result.subjectId ?? result.userId;
|
|
55
158
|
const actorNamespace = activityActorNamespace(provider);
|
|
56
|
-
const derivedPrincipal = result.userId && actorNamespace
|
|
57
|
-
|
|
58
|
-
: undefined;
|
|
59
|
-
const principal = validIdentityReference(result.principal)
|
|
60
|
-
? result.principal
|
|
61
|
-
: derivedPrincipal;
|
|
159
|
+
const derivedPrincipal = result.userId && actorNamespace ? { namespace: actorNamespace, id: result.userId } : undefined;
|
|
160
|
+
const principal = validIdentityReference(result.principal) ? result.principal : derivedPrincipal;
|
|
62
161
|
const subject = subjectId
|
|
63
162
|
? { namespace: actorNamespace ?? `connecta:auth:${provider.kind}`, id: subjectId }
|
|
64
163
|
: principal;
|
|
65
164
|
const interactive = Boolean(result.userId && provider.interactiveOperator);
|
|
165
|
+
// Principal-only API callers still need a stable typed actor for history
|
|
166
|
+
// ownership. Never collapse distinct personal partitions into { kind }.
|
|
167
|
+
const actorId = subjectId ?? principal?.id;
|
|
168
|
+
const recordNamespace = actorNamespace ?? (subjectId === undefined ? principal?.namespace : undefined);
|
|
66
169
|
const actor = {
|
|
67
170
|
kind: provider.kind,
|
|
68
|
-
...(
|
|
69
|
-
...(
|
|
171
|
+
...(actorId ? { id: actorId } : {}),
|
|
172
|
+
...(actorId && recordNamespace ? { namespace: recordNamespace } : {}),
|
|
70
173
|
};
|
|
71
174
|
const identity = {
|
|
72
175
|
actor,
|
|
@@ -81,22 +184,30 @@ export async function authorize(request, baseUrl, auth, runtimeContext, identity
|
|
|
81
184
|
let access;
|
|
82
185
|
try {
|
|
83
186
|
if (identityConfig?.activityAccess) {
|
|
84
|
-
operator =
|
|
85
|
-
? await identityConfig.activityAccess(principal)
|
|
86
|
-
: false;
|
|
187
|
+
operator = principal ? await identityConfig.activityAccess(principal) : false;
|
|
87
188
|
}
|
|
88
189
|
access = parseConnectorAccess(identityConfig?.connectorAccess ? await identityConfig.connectorAccess(identity) : "all", { allowReadOnly: true });
|
|
89
190
|
if (interactive) {
|
|
90
|
-
accessTokenManagement = identityConfig?.accessTokenManagement
|
|
191
|
+
accessTokenManagement = identityConfig?.accessTokenManagement
|
|
192
|
+
? await identityConfig.accessTokenManagement(identity)
|
|
193
|
+
: false;
|
|
91
194
|
if (typeof accessTokenManagement !== "boolean")
|
|
92
195
|
throw new Error("invalid token management permission");
|
|
93
|
-
credentialAdministration = identityConfig?.credentialAdministration
|
|
94
|
-
|
|
196
|
+
credentialAdministration = identityConfig?.credentialAdministration
|
|
197
|
+
? await identityConfig.credentialAdministration(identity)
|
|
198
|
+
: "none";
|
|
199
|
+
personalConnection =
|
|
200
|
+
principal && identityConfig?.personalConnection
|
|
201
|
+
? await identityConfig.personalConnection(identity)
|
|
202
|
+
: "none";
|
|
95
203
|
}
|
|
96
204
|
if (typeof operator !== "boolean")
|
|
97
205
|
throw new Error("invalid activity permission");
|
|
98
206
|
for (const permission of [credentialAdministration, personalConnection]) {
|
|
99
|
-
if (permission !== "all" &&
|
|
207
|
+
if (permission !== "all" &&
|
|
208
|
+
permission !== "none" &&
|
|
209
|
+
(!Array.isArray(permission) ||
|
|
210
|
+
!permission.every((id) => typeof id === "string" && /^[a-z0-9_-]+$/.test(id))))
|
|
100
211
|
throw new Error("invalid identity permission");
|
|
101
212
|
}
|
|
102
213
|
}
|
|
@@ -110,57 +221,39 @@ export async function authorize(request, baseUrl, auth, runtimeContext, identity
|
|
|
110
221
|
ok: true,
|
|
111
222
|
actor,
|
|
112
223
|
identity,
|
|
113
|
-
...(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
...(principal && partitionIdentity
|
|
117
|
-
? { principalKey: await identityStorageKey(principal) }
|
|
118
|
-
: {}),
|
|
224
|
+
...(result.sessionCookies?.length ? { sessionCookies: result.sessionCookies } : {}),
|
|
225
|
+
...(subject && partitionIdentity ? { subjectKey: await identityStorageKey(subject) } : {}),
|
|
226
|
+
...(principal && partitionIdentity ? { principalKey: await identityStorageKey(principal) } : {}),
|
|
119
227
|
...access,
|
|
120
228
|
accessTokenManagement,
|
|
121
229
|
credentialAdministration,
|
|
122
230
|
personalConnection,
|
|
123
231
|
operator,
|
|
124
|
-
...(operator ? { uiAdminEligible: true } : {}),
|
|
232
|
+
...(interactive && operator ? { uiAdminEligible: true } : {}),
|
|
125
233
|
};
|
|
126
234
|
}
|
|
127
235
|
lastResponse = result.response;
|
|
236
|
+
if (recognized || result.response.status !== 401)
|
|
237
|
+
break;
|
|
128
238
|
}
|
|
129
239
|
return {
|
|
130
240
|
ok: false,
|
|
131
|
-
response: lastResponse ??
|
|
132
|
-
|
|
241
|
+
response: await providerChallenge(lastResponse ??
|
|
242
|
+
privateJson({ error: "unauthorized" }, {
|
|
133
243
|
status: 401,
|
|
134
|
-
headers: {
|
|
135
|
-
|
|
136
|
-
"WWW-Authenticate": "Bearer",
|
|
137
|
-
},
|
|
138
|
-
}),
|
|
244
|
+
headers: { "WWW-Authenticate": "Bearer" },
|
|
245
|
+
}), request, baseUrl, auth, explicit ? candidates[0] : undefined),
|
|
139
246
|
};
|
|
140
247
|
}
|
|
248
|
+
/** Human routes reject machine credentials before any verification or storage I/O. */
|
|
141
249
|
export async function authorizeUiIdentity(request, baseUrl, auth, purpose, runtimeContext, identityConfig) {
|
|
142
|
-
|
|
143
|
-
if (providers.length === 0) {
|
|
250
|
+
if (!auth.some((provider) => provider.interactiveOperator)) {
|
|
144
251
|
return {
|
|
145
252
|
ok: false,
|
|
146
253
|
response: privateJson({ error: `${purpose} requires interactive user authentication` }, { status: 403 }),
|
|
147
254
|
};
|
|
148
255
|
}
|
|
149
|
-
|
|
150
|
-
for (const provider of providers) {
|
|
151
|
-
const authz = await authorize(request, baseUrl, [provider], runtimeContext, identityConfig);
|
|
152
|
-
if (!authz.ok) {
|
|
153
|
-
lastResponse = authz.response;
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
if (authz.identity.interactive)
|
|
157
|
-
return authz;
|
|
158
|
-
lastResponse = privateJson({ error: "authenticated user required" }, { status: 403 });
|
|
159
|
-
}
|
|
160
|
-
return {
|
|
161
|
-
ok: false,
|
|
162
|
-
response: lastResponse ?? privateJson({ error: `${purpose} requires interactive user authentication` }, { status: 403 }),
|
|
163
|
-
};
|
|
256
|
+
return authorize(request, baseUrl, auth, runtimeContext, identityConfig, true, true);
|
|
164
257
|
}
|
|
165
258
|
export function isSameOrigin(request, baseUrl) {
|
|
166
259
|
const origin = request.headers.get("origin");
|
|
@@ -174,7 +267,9 @@ export function isSameOrigin(request, baseUrl) {
|
|
|
174
267
|
}
|
|
175
268
|
}
|
|
176
269
|
export function withSecurityHeaders(response, requestUrl, _path) {
|
|
177
|
-
const headers =
|
|
270
|
+
const headers = response.headers.get("Content-Type")?.toLowerCase().startsWith("text/html")
|
|
271
|
+
? htmlSecurityHeaders(response.headers)
|
|
272
|
+
: new Headers(response.headers);
|
|
178
273
|
headers.set("X-Content-Type-Options", "nosniff");
|
|
179
274
|
headers.set("Referrer-Policy", "no-referrer");
|
|
180
275
|
if (requestUrl.protocol === "https:") {
|
|
@@ -188,44 +283,29 @@ export function withSecurityHeaders(response, requestUrl, _path) {
|
|
|
188
283
|
}
|
|
189
284
|
/** Unknown configured ids refuse the complete view, including management rights. */
|
|
190
285
|
export function validateAuthPermissions(authz, registry) {
|
|
191
|
-
for (const value of [
|
|
192
|
-
authz.connectorIds,
|
|
193
|
-
authz.credentialAdministration,
|
|
194
|
-
authz.personalConnection,
|
|
195
|
-
]) {
|
|
286
|
+
for (const value of [authz.connectorIds, authz.credentialAdministration, authz.personalConnection]) {
|
|
196
287
|
if (value === "all" || value === "none")
|
|
197
288
|
continue;
|
|
198
|
-
if (!Array.isArray(value) || value.some(id => !registry.getConnector(id))) {
|
|
289
|
+
if (!Array.isArray(value) || value.some((id) => !registry.getConnector(id))) {
|
|
199
290
|
throw new Error("invalid identity permission connector ids");
|
|
200
291
|
}
|
|
201
292
|
}
|
|
202
293
|
}
|
|
203
|
-
/**
|
|
204
|
-
* Whether this identity may open artifact pages: exactly whoever may read the
|
|
205
|
-
* `artifacts` connector — it is visible to them and `get_artifact` is among
|
|
206
|
-
* its tools they were granted. Viewing grants nothing a program could not
|
|
207
|
-
* already read (ethos, "Human routes manage auth, never capability").
|
|
208
|
-
*/
|
|
209
|
-
export function mayViewArtifacts(authz, registry) {
|
|
210
|
-
if (authz.connectorIds !== "all" && !authz.connectorIds.includes("artifacts")) {
|
|
211
|
-
return false;
|
|
212
|
-
}
|
|
213
|
-
const tools = authz.toolAccess?.get("artifacts");
|
|
214
|
-
if (tools && !tools.has("get_artifact"))
|
|
215
|
-
return false;
|
|
216
|
-
if (!authz.guardedToolAccess?.get("artifacts")?.has("get_artifact"))
|
|
217
|
-
return true;
|
|
218
|
-
return registry.getConnector("artifacts")?.staticTools?.some((tool) => tool.name === "get_artifact" && isExplicitlyReadOnly(tool)) === true;
|
|
219
|
-
}
|
|
220
294
|
export function mayManageConnector(authz, connector) {
|
|
221
295
|
if (!authz.identity.interactive)
|
|
222
296
|
return false;
|
|
223
297
|
if (authz.connectorIds !== "all" && !authz.connectorIds.includes(connector.id)) {
|
|
224
298
|
return false;
|
|
225
299
|
}
|
|
226
|
-
const permission = connector.authScope === "personal"
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
300
|
+
const permission = connector.authScope === "personal" ? authz.personalConnection : authz.credentialAdministration;
|
|
301
|
+
return permission === "all" || (permission !== "none" && permission.includes(connector.id));
|
|
302
|
+
}
|
|
303
|
+
/** Preserve refreshed browser cookies on the redirect or completion page. */
|
|
304
|
+
export function withSessionCookies(response, cookies) {
|
|
305
|
+
if (!cookies?.length)
|
|
306
|
+
return response;
|
|
307
|
+
const headers = new Headers(response.headers);
|
|
308
|
+
for (const cookie of cookies)
|
|
309
|
+
headers.append("Set-Cookie", cookie);
|
|
310
|
+
return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
|
|
231
311
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ProtocolError } from "@modelcontextprotocol/server";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { ConnectorCallError } from "../errors.js";
|
|
4
|
+
import { SkillsRegistry } from "../skills.js";
|
|
5
|
+
/** Native Skills methods use the same request-view registry as both text readers. */
|
|
6
|
+
export function registerSkills(server, view, baseUrl, options) {
|
|
7
|
+
// McpServer's resource constructor installs its static registry and turns
|
|
8
|
+
// listChanged on. Declare the capability on the low-level server instead.
|
|
9
|
+
server.server.registerCapabilities({ resources: {} });
|
|
10
|
+
const registry = new SkillsRegistry(view, baseUrl, options);
|
|
11
|
+
const listParams = z.object({ cursor: z.string().max(128).optional() });
|
|
12
|
+
const getParams = z.object({ uri: z.string().min(1).max(32_768).startsWith("skill://") });
|
|
13
|
+
const result = z.record(z.string(), z.unknown());
|
|
14
|
+
const safely = async (read) => {
|
|
15
|
+
try {
|
|
16
|
+
return await read();
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (error instanceof ConnectorCallError && (error.code === "not_found" || error.code === "invalid_args")) {
|
|
20
|
+
throw new ProtocolError(-32602, "Unknown skill/file or invalid skills cursor.");
|
|
21
|
+
}
|
|
22
|
+
// No raw downstream cause can become an SDK/operator diagnostic.
|
|
23
|
+
throw new ProtocolError(-32603, "Skills are unavailable.");
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
server.server.setRequestHandler("skills/list", { params: listParams, result }, (params) => safely(() => registry.list(params.cursor)));
|
|
27
|
+
server.server.setRequestHandler("skills/get", { params: getParams, result }, (params) => safely(() => registry.get(params.uri)));
|
|
28
|
+
server.server.setRequestHandler("resources/list", { params: listParams, result }, (params) => safely(() => registry.resources(params.cursor)));
|
|
29
|
+
server.server.setRequestHandler("resources/read", { params: getParams, result }, (params) => safely(() => registry.read(params.uri)));
|
|
30
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { activityEventVisible } from "./activity-disclosure.js";
|
|
2
|
+
import { operatorActivityContext } from "./activity-context.js";
|
|
3
|
+
import { bindActivityRequest } from "../activity-request.js";
|
|
4
|
+
import { configValuePaths } from "../config-value-sources.js";
|
|
5
|
+
import { Effect } from "effect";
|
|
6
|
+
import { intersectAccess } from "../connector-access.js";
|
|
7
|
+
import { describedTools } from "../described.js";
|
|
8
|
+
import { resolveDiscoveryConcurrency } from "../concurrency.js";
|
|
9
|
+
import { recordedToolName } from "../operator-record.js";
|
|
10
|
+
import { closeScope } from "../runtime/connector-scope.js";
|
|
11
|
+
import { withDeadlineEffect } from "../runtime/run.js";
|
|
12
|
+
import { uiProblemFor } from "../ui.js";
|
|
13
|
+
import { authorized, serveOperator, visibleRegistry } from "./operator.js";
|
|
14
|
+
import { mayManageConnector, privateJson } from "./shared.js";
|
|
15
|
+
const attempt = (operation) => Effect.tryPromise({ try: operation, catch: (error) => error });
|
|
16
|
+
const validName = (name) => recordedToolName({ name }) === name;
|
|
17
|
+
function grants(access, ids) {
|
|
18
|
+
return ids
|
|
19
|
+
.filter((id) => access.connectorIds === "all" || access.connectorIds.includes(id))
|
|
20
|
+
.map((connectorId) => {
|
|
21
|
+
const tools = access.toolAccess?.get(connectorId);
|
|
22
|
+
return {
|
|
23
|
+
connectorId,
|
|
24
|
+
tools: tools
|
|
25
|
+
? [...tools].filter(validName).map((name) => ({
|
|
26
|
+
name,
|
|
27
|
+
requireReadOnly: access.guardedToolAccess?.get(connectorId)?.has(name) === true,
|
|
28
|
+
}))
|
|
29
|
+
: "all",
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function toolVisible(authz, id, name, classification) {
|
|
34
|
+
return (validName(name) &&
|
|
35
|
+
(!authz.toolAccess?.get(id) || authz.toolAccess.get(id).has(name)) &&
|
|
36
|
+
(!authz.guardedToolAccess?.get(id)?.has(name) || classification === "read"));
|
|
37
|
+
}
|
|
38
|
+
function observe(context, registry, connector, activity) {
|
|
39
|
+
const { opts, baseUrl, request, defer } = context;
|
|
40
|
+
const scope = {};
|
|
41
|
+
if (activity)
|
|
42
|
+
bindActivityRequest(scope, activity);
|
|
43
|
+
const id = connector.id;
|
|
44
|
+
return withDeadlineEffect((signal) => Effect.gen(function* () {
|
|
45
|
+
const drift = yield* attempt(() => registry.credentialDriftFor(id));
|
|
46
|
+
const status = drift
|
|
47
|
+
? { state: "auth_required" }
|
|
48
|
+
: yield* attempt(() => registry.statusFor(id, baseUrl, scope, { signal, ...(defer ? { defer } : {}) }));
|
|
49
|
+
let catalogFailed = false;
|
|
50
|
+
const tools = status.state === "ok"
|
|
51
|
+
? yield* attempt(() => registry.getTools(id, baseUrl, scope, { signal, ...(defer ? { defer } : {}) })).pipe(Effect.catch(() => {
|
|
52
|
+
catalogFailed = true;
|
|
53
|
+
return Effect.succeed([]);
|
|
54
|
+
}))
|
|
55
|
+
: [];
|
|
56
|
+
const problem = uiProblemFor(connector, status.state, { credentialDrift: Boolean(drift), catalogFailed });
|
|
57
|
+
return {
|
|
58
|
+
id,
|
|
59
|
+
status: status.state,
|
|
60
|
+
...(status.registrationPath ? { auth: { registrationPath: status.registrationPath } } : {}),
|
|
61
|
+
...(problem ? { problem } : {}),
|
|
62
|
+
// Catalog metadata is allowed for authenticated operators. Only the name
|
|
63
|
+
// and its derived address are withheld when they fail the record grammar.
|
|
64
|
+
tools: describedTools(tools).map((tool) => {
|
|
65
|
+
const name = recordedToolName(tool);
|
|
66
|
+
return {
|
|
67
|
+
...tool,
|
|
68
|
+
name,
|
|
69
|
+
address: `${id}.${name}`,
|
|
70
|
+
classification: tool.classification === "read" ? "read" : "write",
|
|
71
|
+
};
|
|
72
|
+
}),
|
|
73
|
+
catalogAgeMs: registry.catalogAgeMs(id),
|
|
74
|
+
lastCall: null,
|
|
75
|
+
};
|
|
76
|
+
}), {
|
|
77
|
+
timeoutMs: opts.config.discovery.probeTimeoutMs,
|
|
78
|
+
signal: request.signal,
|
|
79
|
+
timeoutError: new Error("Operator config observation timed out."),
|
|
80
|
+
}).pipe(Effect.catchDefect((defect) => Effect.fail(defect)), Effect.catch(() => Effect.succeed({
|
|
81
|
+
id,
|
|
82
|
+
status: "error",
|
|
83
|
+
problem: "connector_unavailable",
|
|
84
|
+
tools: [],
|
|
85
|
+
catalogAgeMs: registry.catalogAgeMs(id),
|
|
86
|
+
lastCall: null,
|
|
87
|
+
})), Effect.ensuring(Effect.suspend(() => closeScope(connector, registry.contextFor(id, baseUrl, scope), defer))));
|
|
88
|
+
}
|
|
89
|
+
const OUTCOMES = new Set([
|
|
90
|
+
"success",
|
|
91
|
+
"error",
|
|
92
|
+
"timeout",
|
|
93
|
+
"cancelled",
|
|
94
|
+
"paused",
|
|
95
|
+
"approved",
|
|
96
|
+
]);
|
|
97
|
+
const isTimestamp = (value) => /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$/.test(value) && Number.isFinite(Date.parse(value));
|
|
98
|
+
function lastCalls(context, authz, registry, rows, admittedPools) {
|
|
99
|
+
const activity = context.opts.config.activity;
|
|
100
|
+
if (!activity?.store.list)
|
|
101
|
+
return Effect.succeed("unconfigured");
|
|
102
|
+
if (!authz.operator)
|
|
103
|
+
return Effect.succeed("forbidden");
|
|
104
|
+
return Effect.gen(function* () {
|
|
105
|
+
const permitted = activity.readGate
|
|
106
|
+
? yield* attempt(async () => (await activity.readGate(authz.actor)) === true).pipe(Effect.orElseSucceed(() => false))
|
|
107
|
+
: true;
|
|
108
|
+
if (!permitted)
|
|
109
|
+
return "forbidden";
|
|
110
|
+
return yield* Effect.gen(function* () {
|
|
111
|
+
const byId = new Map(rows.map((row) => [row.id, row]));
|
|
112
|
+
let cursor;
|
|
113
|
+
const seen = new Set();
|
|
114
|
+
// Bound the read even when a custom reader repeats a cursor or ignores
|
|
115
|
+
// its requested page size. Each page belongs to the request fiber, so
|
|
116
|
+
// cancellation cannot start the next page.
|
|
117
|
+
for (let pageIndex = 0; pageIndex < 10; pageIndex++) {
|
|
118
|
+
const page = yield* attempt(() => activity.store.list({ limit: 100, ...(cursor ? { cursor } : {}) }));
|
|
119
|
+
for (const event of page.events.slice(0, 100)) {
|
|
120
|
+
if (event.kind === "catalog_drift")
|
|
121
|
+
continue;
|
|
122
|
+
const row = byId.get(event.connectorId);
|
|
123
|
+
if (!row || !isTimestamp(event.occurredAt) || !OUTCOMES.has(event.outcome))
|
|
124
|
+
continue;
|
|
125
|
+
if (!activityEventVisible(context, authz, registry, admittedPools, event))
|
|
126
|
+
continue;
|
|
127
|
+
if (!row.lastCall || event.occurredAt > row.lastCall.at)
|
|
128
|
+
row.lastCall = { at: event.occurredAt, outcome: event.outcome };
|
|
129
|
+
}
|
|
130
|
+
cursor = page.nextCursor;
|
|
131
|
+
if (!cursor || seen.has(cursor))
|
|
132
|
+
break;
|
|
133
|
+
seen.add(cursor);
|
|
134
|
+
}
|
|
135
|
+
return "available";
|
|
136
|
+
}).pipe(Effect.catchDefect((defect) => Effect.fail(defect)), Effect.orElseSucceed(() => {
|
|
137
|
+
for (const row of rows)
|
|
138
|
+
row.lastCall = null;
|
|
139
|
+
return "unavailable";
|
|
140
|
+
}));
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
function configRead(context) {
|
|
144
|
+
const { opts } = context;
|
|
145
|
+
return Effect.gen(function* () {
|
|
146
|
+
const authz = yield* authorized(context);
|
|
147
|
+
const registry = yield* visibleRegistry(context, authz);
|
|
148
|
+
const visible = registry.listConnectors();
|
|
149
|
+
const ids = visible.map((c) => c.id);
|
|
150
|
+
const pools = [];
|
|
151
|
+
for (const [name, pool] of opts.pools) {
|
|
152
|
+
const admitted = yield* attempt(async () => (await pool.grant(authz.identity)) === true).pipe(Effect.orElseSucceed(() => false));
|
|
153
|
+
if (admitted)
|
|
154
|
+
pools.push({
|
|
155
|
+
name,
|
|
156
|
+
path: `/mcp/${name}`,
|
|
157
|
+
trust: pool.trust,
|
|
158
|
+
grants: grants(intersectAccess(authz, pool.access), ids),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
const config = structuredClone(opts.configDescription);
|
|
162
|
+
config.connectors = config.connectors
|
|
163
|
+
.filter((c) => ids.includes(c.id))
|
|
164
|
+
.map((c) => ({
|
|
165
|
+
...c,
|
|
166
|
+
...(c.tools
|
|
167
|
+
? { tools: c.tools.filter((tool) => toolVisible(authz, c.id, tool.name, tool.classification)) }
|
|
168
|
+
: {}),
|
|
169
|
+
}));
|
|
170
|
+
config.classification = Object.fromEntries(Object.entries(config.classification)
|
|
171
|
+
.filter(([id]) => ids.includes(id))
|
|
172
|
+
.map(([id, tools]) => [
|
|
173
|
+
id,
|
|
174
|
+
Object.fromEntries(Object.entries(tools).filter(([name, classification]) => toolVisible(authz, id, name, classification))),
|
|
175
|
+
]));
|
|
176
|
+
config.pools = config.pools.flatMap((pool) => {
|
|
177
|
+
const admitted = pools.find((p) => p.name === pool.name);
|
|
178
|
+
if (!admitted)
|
|
179
|
+
return [];
|
|
180
|
+
return [
|
|
181
|
+
{
|
|
182
|
+
...pool,
|
|
183
|
+
tools: admitted.grants.flatMap((grant) => grant.tools === "all"
|
|
184
|
+
? [grant.connectorId]
|
|
185
|
+
: grant.tools.map((tool) => `${grant.connectorId}.${tool.name}`)),
|
|
186
|
+
},
|
|
187
|
+
];
|
|
188
|
+
});
|
|
189
|
+
const requestActivity = operatorActivityContext(context, authz);
|
|
190
|
+
const rows = yield* Effect.forEach(visible, (c) => observe(context, registry, c, requestActivity), {
|
|
191
|
+
concurrency: resolveDiscoveryConcurrency(opts.config.discovery.concurrency),
|
|
192
|
+
});
|
|
193
|
+
const activity = yield* lastCalls(context, authz, registry, rows, new Set(pools.map((pool) => pool.name)));
|
|
194
|
+
const contract = {
|
|
195
|
+
schemaVersion: 1,
|
|
196
|
+
config,
|
|
197
|
+
configSources: Object.fromEntries(configValuePaths(config).map((path) => [path, opts.configValueSources?.[path] ?? "config"])),
|
|
198
|
+
live: { connectors: rows, activity },
|
|
199
|
+
you: {
|
|
200
|
+
interactive: authz.identity.interactive,
|
|
201
|
+
grants: grants(authz, ids),
|
|
202
|
+
trust: opts.config.trust,
|
|
203
|
+
pools,
|
|
204
|
+
permissions: {
|
|
205
|
+
activity: activity === "available" || activity === "unavailable",
|
|
206
|
+
accessTokenManagement: Boolean(opts.config.accessTokens && authz.accessTokenManagement && authz.identity.principal),
|
|
207
|
+
connectors: visible.map((c) => ({
|
|
208
|
+
id: c.id,
|
|
209
|
+
use: true,
|
|
210
|
+
manageSharedAuth: c.authScope !== "personal" && mayManageConnector(authz, c),
|
|
211
|
+
connectPersonal: c.authScope === "personal" && mayManageConnector(authz, c),
|
|
212
|
+
})),
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
return privateJson(contract);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
export async function routeUiConfig(context) {
|
|
220
|
+
if (context.path !== "/ui/api/config")
|
|
221
|
+
return null;
|
|
222
|
+
if (context.request.method !== "GET")
|
|
223
|
+
return privateJson({ error: "method not allowed" }, { status: 405 });
|
|
224
|
+
return serveOperator(configRead(context), context.request.signal);
|
|
225
|
+
}
|