@zackbart/connecta 0.31.0 → 0.33.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 +97 -0
- package/README.md +30 -20
- package/bin/connecta.mjs +25 -6
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.d.ts +167 -23
- package/dist/access-tokens.js +676 -140
- package/dist/agent-identity.js +47 -0
- package/dist/auth/cloudflare-access.js +13 -5
- package/dist/auth-elicitation.js +1 -1
- package/dist/call-admission-context.js +15 -0
- package/dist/call-admission.js +30 -43
- package/dist/call-recovery.js +6 -2
- package/dist/catalog-service.js +7 -2
- package/dist/config-value-sources.js +14 -9
- package/dist/config.d.ts +58 -78
- package/dist/config.js +143 -47
- package/dist/connector-access.d.ts +4 -33
- package/dist/connector-access.js +15 -99
- package/dist/connector-caller.d.ts +7 -1
- package/dist/connector-caller.js +3 -2
- package/dist/connectors/api-connector.d.ts +27 -4
- package/dist/connectors/api-connector.js +2 -0
- package/dist/connectors/guarded-fetch.d.ts +9 -0
- package/dist/connectors/guarded-fetch.js +7 -0
- package/dist/connectors/remote-mcp.js +2 -0
- package/dist/describe-config.d.ts +32 -7
- package/dist/describe-config.js +9 -18
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -0
- package/dist/execute.js +16 -14
- package/dist/index.d.ts +4 -3
- package/dist/index.js +30 -10
- package/dist/invocation.js +12 -7
- package/dist/membership.d.ts +51 -0
- package/dist/membership.js +121 -0
- package/dist/meta-tools.js +17 -19
- package/dist/module-contracts.d.ts +23 -6
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +2 -2
- package/dist/operator-ui/agents.d.ts +74 -0
- package/dist/operator-ui/agents.js +104 -0
- package/dist/operator-ui/contract.d.ts +151 -29
- package/dist/operator-ui/endpoints.js +192 -0
- package/dist/operator-ui/fix-prompts.js +4 -4
- package/dist/operator-ui/generated.js +5 -4
- package/dist/operator-ui/member-page.js +112 -0
- package/dist/operator-ui/model.d.ts +15 -7
- package/dist/operator-ui/view.d.ts +84 -19
- package/dist/operator-ui/view.js +83 -21
- package/dist/page-styles.js +1 -1
- package/dist/program-writes.js +2 -2
- package/dist/provider.d.ts +63 -11
- package/dist/provider.js +46 -7
- package/dist/providers/_shared/google/workspace.d.ts +53 -16
- package/dist/providers/_shared/google/workspace.js +31 -9
- package/dist/providers/_shared/rest/tools.js +2 -2
- package/dist/providers/_shared/rest/value-safety.d.ts +50 -0
- package/dist/providers/_shared/rest/value-safety.js +699 -7
- package/dist/providers/breeze/skill.generated.js +2 -2
- package/dist/providers/ccb/index.js +1 -1
- package/dist/providers/cloudflare/index.js +3 -3
- package/dist/providers/cloudflare/rest.js +25 -3
- package/dist/providers/cloudflare/skill.generated.js +2 -2
- package/dist/providers/cloudflare/value-safety.js +32 -0
- package/dist/providers/docs/index.d.ts +1 -1
- package/dist/providers/docs/index.js +5 -5
- package/dist/providers/docs/skill.generated.js +2 -2
- package/dist/providers/drive/index.d.ts +1 -1
- package/dist/providers/drive/index.js +4 -4
- package/dist/providers/drive/skill.generated.js +2 -2
- package/dist/providers/forms/index.d.ts +1 -1
- package/dist/providers/forms/index.js +5 -5
- package/dist/providers/forms/skill.generated.js +2 -2
- package/dist/providers/github/skill.generated.js +2 -2
- package/dist/providers/gmail/index.d.ts +1 -1
- package/dist/providers/gmail/index.js +279 -26
- package/dist/providers/gmail/skill.generated.js +2 -2
- package/dist/providers/linear/index.js +1 -1
- package/dist/providers/mixpanel/index.d.ts +27 -12
- package/dist/providers/mixpanel/index.js +58 -6
- package/dist/providers/mixpanel/operations.js +20 -0
- package/dist/providers/mixpanel/rest.js +603 -0
- package/dist/providers/mixpanel/skill.generated.js +3 -2
- package/dist/providers/mixpanel/value-safety.js +16 -0
- package/dist/providers/notion/index.d.ts +7 -0
- package/dist/providers/notion/index.js +116 -11
- package/dist/providers/notion/rest.js +183 -24
- package/dist/providers/notion/skill.generated.js +4 -4
- package/dist/providers/notion/value-safety.js +30 -5
- package/dist/providers/overflow/index.js +1 -1
- package/dist/providers/planning-center/index.d.ts +27 -0
- package/dist/providers/planning-center/index.js +287 -25
- package/dist/providers/planning-center/skill.generated.js +24 -4
- package/dist/providers/sheets/index.d.ts +4 -1
- package/dist/providers/sheets/index.js +9 -6
- package/dist/providers/sheets/skill.generated.js +2 -2
- package/dist/providers/slides/index.d.ts +1 -1
- package/dist/providers/slides/index.js +6 -6
- package/dist/providers/slides/skill.generated.js +2 -2
- package/dist/providers/stripe/rest.js +9 -2
- package/dist/providers/stripe/skill.generated.js +2 -2
- package/dist/providers/stripe/value-safety.js +22 -0
- package/dist/providers/vercel/index.js +2 -2
- package/dist/providers/vercel/rest.js +20 -2
- package/dist/providers/vercel/skill.generated.js +2 -2
- package/dist/providers/vercel/value-safety.js +22 -0
- package/dist/providers/wispr-flow/index.d.ts +14 -0
- package/dist/providers/wispr-flow/index.js +117 -0
- package/dist/providers/wispr-flow/skill.generated.js +8 -0
- package/dist/registry.d.ts +2 -3
- package/dist/registry.js +8 -9
- package/dist/routes/access-tokens.js +82 -75
- package/dist/routes/activity-disclosure.js +17 -18
- package/dist/routes/activity.js +34 -54
- package/dist/routes/actor-labels.js +33 -4
- package/dist/routes/agent-path.js +43 -0
- package/dist/routes/connect.js +16 -12
- package/dist/routes/credentials.js +16 -40
- package/dist/routes/mcp.js +155 -85
- package/dist/routes/oauth-management.js +11 -5
- package/dist/routes/oauth.js +3 -9
- package/dist/routes/operator.js +25 -17
- package/dist/routes/shared.d.ts +60 -20
- package/dist/routes/shared.js +187 -96
- package/dist/routes/ui-config.js +437 -97
- package/dist/routes/ui-data.js +2 -2
- package/dist/routes/ui.js +9 -16
- package/dist/runtime/call-admission.js +9 -0
- package/dist/server.js +8 -0
- package/dist/skills.js +5 -4
- package/dist/storage/keys.js +9 -1
- package/dist/tool-safety.js +13 -5
- package/dist/types.d.ts +12 -1
- package/dist/ui.d.ts +2 -2
- package/dist/ui.js +17 -15
- package/dist/usage-guide.js +3 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/workspace-address.js +9 -0
- package/dist/workspace-subject.js +23 -0
- package/documentation/README.md +46 -22
- package/documentation/architecture.md +378 -58
- package/documentation/auth.md +806 -215
- package/documentation/code-mode.md +93 -81
- package/documentation/deploying.md +128 -26
- package/documentation/integrating.md +145 -22
- package/documentation/meta-tools.md +58 -46
- package/documentation/migration-0.33.md +344 -0
- package/documentation/operating.md +14 -9
- package/documentation/operator-tests.md +117 -31
- package/documentation/operator-ui.md +326 -55
- package/documentation/provider-auth-migration.md +42 -27
- package/documentation/provider-migration-0.29.md +3 -3
- package/examples/worker/AGENTS.md +35 -8
- package/examples/worker/README.md +92 -53
- package/examples/worker/src/connecta.config.ts +41 -20
- package/examples/worker/wrangler.jsonc +3 -0
- package/package.json +7 -2
- package/templates/node/.env.example +3 -0
- package/templates/node/AGENTS.md +33 -10
- package/templates/node/Dockerfile +2 -2
- package/templates/node/README.md +71 -44
- package/templates/node/docker-compose.yml +3 -2
- package/templates/node/package.json +4 -3
- package/templates/node/src/adopt-token.ts +23 -0
- package/templates/node/src/connecta.config.ts +33 -12
- package/templates/node/src/rotate-token.ts +17 -0
- package/dist/tool-safety.d.ts +0 -9
- package/templates/node/src/provision-token.ts +0 -17
package/dist/routes/shared.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { ActivityActor } from "../activity.js";
|
|
2
2
|
import type { DeferredWork } from "../connector-scope.js";
|
|
3
3
|
import type { AdmissionController } from "../executor-admission.js";
|
|
4
|
-
import type { Registry
|
|
5
|
-
import type
|
|
6
|
-
import type { AuthenticatedIdentity, Executor, InboundAuth, InboundAuthRuntimeContext } from "../types.js";
|
|
7
|
-
import type {
|
|
4
|
+
import type { Registry } from "../registry.js";
|
|
5
|
+
import { type ResolvedPool } from "../membership.js";
|
|
6
|
+
import type { AuthenticatedIdentity, Executor, IdentityReference, InboundAuth, InboundAuthRuntimeContext } from "../types.js";
|
|
7
|
+
import type { ResolvedConfig } from "../config.js";
|
|
8
8
|
export { msg } from "../errors.js";
|
|
9
9
|
export interface ServerOptions {
|
|
10
10
|
/**
|
|
@@ -17,6 +17,8 @@ export interface ServerOptions {
|
|
|
17
17
|
configDescription: import("../describe-config.js").ConnectaConfigDescription;
|
|
18
18
|
configValueSources?: Readonly<Record<string, "default" | "config">>;
|
|
19
19
|
registry: Registry;
|
|
20
|
+
/** Resolved `maintainers`: the only principals plain `/mcp` and operator routes admit. */
|
|
21
|
+
maintainers: readonly IdentityReference[];
|
|
20
22
|
/** Validated named pools served at `/mcp/<name>`; empty when none declared. */
|
|
21
23
|
pools: ReadonlyMap<string, ResolvedPool>;
|
|
22
24
|
/** Required sandbox backing execute_code, wrapped in fallback admission if needed. */
|
|
@@ -39,6 +41,14 @@ export interface RouteContext {
|
|
|
39
41
|
runtimeContext: RuntimeExecutionContext | undefined;
|
|
40
42
|
}
|
|
41
43
|
export declare function privateJson(body: unknown, init?: ResponseInit): Response;
|
|
44
|
+
/**
|
|
45
|
+
* The body as text, or undefined once it is longer than `maxChars`. No UTF-16
|
|
46
|
+
* unit costs more than three bytes, and a malformed sequence decodes to one
|
|
47
|
+
* U+FFFD per at most three bytes, so a body past three bytes per character is
|
|
48
|
+
* past the limit too — and reading stops there, instead of buffering whatever
|
|
49
|
+
* a client cares to send before measuring it.
|
|
50
|
+
*/
|
|
51
|
+
export declare function boundedText(request: Request, maxChars: number): Promise<string | undefined>;
|
|
42
52
|
/**
|
|
43
53
|
* Bounded, escaped form of a caller-influenced value (an identity id or OAuth
|
|
44
54
|
* callback connector id) for the operator log. Goes
|
|
@@ -55,40 +65,70 @@ export declare function authMetadata(request: Request, baseUrl: string, auth: re
|
|
|
55
65
|
provider: InboundAuth;
|
|
56
66
|
response: Response;
|
|
57
67
|
} | null>;
|
|
58
|
-
export declare function authorize(request: Request, baseUrl: string, auth: readonly InboundAuth[], runtimeContext?: RuntimeExecutionContext,
|
|
68
|
+
export declare function authorize(request: Request, baseUrl: string, auth: readonly InboundAuth[], runtimeContext?: RuntimeExecutionContext, partitionIdentity?: boolean, interactiveOnly?: boolean): Promise<{
|
|
59
69
|
ok: true;
|
|
60
70
|
actor: ActivityActor;
|
|
61
71
|
identity: AuthenticatedIdentity;
|
|
62
72
|
sessionCookies?: readonly string[];
|
|
63
73
|
subjectKey?: string;
|
|
64
74
|
principalKey?: string;
|
|
65
|
-
connectorIds: "all" | readonly string[];
|
|
66
|
-
/** Per-connector tool allowlist for connectors granted by address only. */
|
|
67
|
-
toolAccess?: ToolAccess;
|
|
68
|
-
/** Granted addresses that still require the catalog's read-only hint. */
|
|
69
|
-
guardedToolAccess?: ToolAccess;
|
|
70
|
-
operator: boolean;
|
|
71
|
-
accessTokenManagement: boolean;
|
|
72
|
-
credentialAdministration: ConnectorPermission;
|
|
73
|
-
personalConnection: ConnectorPermission;
|
|
74
|
-
/** Backward-compatible name used by operator views. */
|
|
75
|
-
uiAdminEligible?: boolean;
|
|
76
75
|
} | {
|
|
77
76
|
ok: false;
|
|
78
77
|
response: Response;
|
|
79
78
|
}>;
|
|
79
|
+
/**
|
|
80
|
+
* Admission at an agent endpoint, `/agent/<pool>` (#837). Only the built-in
|
|
81
|
+
* `cta_` adapter is consulted: Clerk and Access never run, cookies and Clerk
|
|
82
|
+
* handshake parameters are stripped, and the runtime context, which carries
|
|
83
|
+
* Access's `ctx.access` even on a bypassed path, is never passed. Anything but
|
|
84
|
+
* an admitted declared agent gets the machine-only challenge.
|
|
85
|
+
*/
|
|
86
|
+
export declare function authorizeAgent(request: Request, baseUrl: string, auth: readonly InboundAuth[]): Promise<Awaited<ReturnType<typeof authorize>>>;
|
|
80
87
|
/** Human routes reject machine credentials before any verification or storage I/O. */
|
|
81
|
-
export declare function authorizeUiIdentity(request: Request, baseUrl: string, auth: readonly InboundAuth[], purpose: string, runtimeContext?: RuntimeExecutionContext
|
|
88
|
+
export declare function authorizeUiIdentity(request: Request, baseUrl: string, auth: readonly InboundAuth[], purpose: string, runtimeContext?: RuntimeExecutionContext): Promise<Awaited<ReturnType<typeof authorize>>>;
|
|
82
89
|
export declare function isSameOrigin(request: Request, baseUrl: string): boolean;
|
|
83
90
|
export declare function withSecurityHeaders(response: Response, requestUrl: URL, _path: string): Response;
|
|
84
91
|
type AuthorizedIdentity = Extract<Awaited<ReturnType<typeof authorize>>, {
|
|
85
92
|
ok: true;
|
|
86
93
|
}>;
|
|
87
|
-
/**
|
|
88
|
-
|
|
89
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Plain `/mcp` and every operator route admit a maintainer (#837). An open
|
|
96
|
+
* deployment has no inbound auth and so no maintainers: its one anonymous
|
|
97
|
+
* caller keeps the whole default endpoint, as the startup warning says.
|
|
98
|
+
*/
|
|
99
|
+
export declare function admitsMaintainer(opts: ServerOptions, authz: AuthorizedIdentity): boolean;
|
|
100
|
+
/**
|
|
101
|
+
* The declared pool `/mcp/<name>` serves to this person, or undefined for the
|
|
102
|
+
* same 404. An agent is never admitted here, even when listed (#837).
|
|
103
|
+
*/
|
|
104
|
+
export declare function memberPool(opts: ServerOptions, name: string, identity: Readonly<AuthenticatedIdentity>): ResolvedPool | undefined;
|
|
105
|
+
/** The declared pool `/agent/<name>` serves to this agent, or undefined for the same 404. */
|
|
106
|
+
export declare function agentPool(opts: ServerOptions, name: string, identity: Readonly<AuthenticatedIdentity>): ResolvedPool | undefined;
|
|
107
|
+
/**
|
|
108
|
+
* The connectors of every pool this identity is a member of (#837): what a
|
|
109
|
+
* pool member may ever manage, and only their personal ones among these.
|
|
110
|
+
*/
|
|
111
|
+
export declare function memberConnectors(opts: ServerOptions, identity: Readonly<AuthenticatedIdentity>): "all" | ReadonlySet<string>;
|
|
112
|
+
/**
|
|
113
|
+
* Whether this identity may start, finish, or remove a connector's auth
|
|
114
|
+
* (#837). Only an interactive person ever may; an agent or any other machine
|
|
115
|
+
* credential manages nothing. Shared auth is deployment-wide, so it takes a
|
|
116
|
+
* maintainer. A personal connector takes a stable principal that is a
|
|
117
|
+
* maintainer, whose plain `/mcp` includes it, or a member of a pool that
|
|
118
|
+
* contains it; either way it is always the caller's own partition, which
|
|
119
|
+
* callers bind by principal. Every path that issues a link, starts,
|
|
120
|
+
* completes, or disconnects auth, or writes a credential, asks this.
|
|
121
|
+
*/
|
|
122
|
+
export declare function mayManageConnector(opts: ServerOptions, authz: AuthorizedIdentity, connector: {
|
|
90
123
|
id: string;
|
|
91
124
|
authScope?: "shared" | "personal";
|
|
92
125
|
}): boolean;
|
|
126
|
+
/** Agent token list, issue, rotate, and revoke: an interactive maintainer only (#837). */
|
|
127
|
+
export declare function managesAgentTokens(opts: ServerOptions, authz: AuthorizedIdentity): boolean;
|
|
128
|
+
/**
|
|
129
|
+
* Activity reads: a maintainer principal, interactive or not (#837). Members
|
|
130
|
+
* and agents read none, and an open deployment has no maintainer.
|
|
131
|
+
*/
|
|
132
|
+
export declare function readsActivity(opts: ServerOptions, authz: AuthorizedIdentity): boolean;
|
|
93
133
|
/** Preserve refreshed browser cookies on the redirect or completion page. */
|
|
94
134
|
export declare function withSessionCookies(response: Response, cookies?: readonly string[]): Response;
|
package/dist/routes/shared.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { authorizationCredential, authorizationRequest, isMachineCredential } from "../inbound-credential.js";
|
|
2
2
|
import { htmlSecurityHeaders } from "../html-security.js";
|
|
3
|
-
import {
|
|
3
|
+
import { isMaintainer, isMember } from "../membership.js";
|
|
4
4
|
import { identityStorageKey, validIdentityReference } from "../identity.js";
|
|
5
|
+
import { assertsAgents, isAgentName } from "../agent-identity.js";
|
|
5
6
|
export { msg } from "../errors.js";
|
|
6
7
|
export function privateJson(body, init = {}) {
|
|
7
8
|
const headers = new Headers(init.headers);
|
|
@@ -10,6 +11,40 @@ export function privateJson(body, init = {}) {
|
|
|
10
11
|
headers.set("Referrer-Policy", "no-referrer");
|
|
11
12
|
return new Response(JSON.stringify(body), { ...init, headers });
|
|
12
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* The body as text, or undefined once it is longer than `maxChars`. No UTF-16
|
|
16
|
+
* unit costs more than three bytes, and a malformed sequence decodes to one
|
|
17
|
+
* U+FFFD per at most three bytes, so a body past three bytes per character is
|
|
18
|
+
* past the limit too — and reading stops there, instead of buffering whatever
|
|
19
|
+
* a client cares to send before measuring it.
|
|
20
|
+
*/
|
|
21
|
+
export async function boundedText(request, maxChars) {
|
|
22
|
+
if (!request.body)
|
|
23
|
+
return "";
|
|
24
|
+
const maxBytes = maxChars * 3;
|
|
25
|
+
const reader = request.body.getReader();
|
|
26
|
+
const chunks = [];
|
|
27
|
+
let size = 0;
|
|
28
|
+
for (;;) {
|
|
29
|
+
const { done, value } = await reader.read();
|
|
30
|
+
if (done)
|
|
31
|
+
break;
|
|
32
|
+
size += value.byteLength;
|
|
33
|
+
if (size > maxBytes) {
|
|
34
|
+
reader.cancel().catch(() => { });
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
chunks.push(value);
|
|
38
|
+
}
|
|
39
|
+
const bytes = new Uint8Array(size);
|
|
40
|
+
let offset = 0;
|
|
41
|
+
for (const chunk of chunks) {
|
|
42
|
+
bytes.set(chunk, offset);
|
|
43
|
+
offset += chunk.byteLength;
|
|
44
|
+
}
|
|
45
|
+
const text = new TextDecoder().decode(bytes);
|
|
46
|
+
return text.length > maxChars ? undefined : text;
|
|
47
|
+
}
|
|
13
48
|
const MAX_LOGGED_VALUE_LENGTH = 64;
|
|
14
49
|
/**
|
|
15
50
|
* Bounded, escaped form of a caller-influenced value (an identity id or OAuth
|
|
@@ -42,6 +77,13 @@ export async function authMetadata(request, baseUrl, auth) {
|
|
|
42
77
|
return null;
|
|
43
78
|
}
|
|
44
79
|
async function providerChallenge(response, request, baseUrl, auth, refusingProvider) {
|
|
80
|
+
const privateHeaders = new Headers(response.headers);
|
|
81
|
+
privateHeaders.set("Cache-Control", "no-store");
|
|
82
|
+
response = new Response(response.body, {
|
|
83
|
+
status: response.status,
|
|
84
|
+
statusText: response.statusText,
|
|
85
|
+
headers: privateHeaders,
|
|
86
|
+
});
|
|
45
87
|
if (response.status !== 401)
|
|
46
88
|
return response;
|
|
47
89
|
if (!response.headers.has("WWW-Authenticate")) {
|
|
@@ -80,7 +122,7 @@ function recognizedProvider(request, auth, runtimeContext) {
|
|
|
80
122
|
}
|
|
81
123
|
return undefined;
|
|
82
124
|
}
|
|
83
|
-
export async function authorize(request, baseUrl, auth, runtimeContext,
|
|
125
|
+
export async function authorize(request, baseUrl, auth, runtimeContext, partitionIdentity = true, interactiveOnly = false) {
|
|
84
126
|
const credential = authorizationCredential(request);
|
|
85
127
|
const explicit = credential.kind !== "absent";
|
|
86
128
|
if (credential.kind === "invalid") {
|
|
@@ -100,56 +142,53 @@ export async function authorize(request, baseUrl, auth, runtimeContext, identity
|
|
|
100
142
|
if (interactiveOnly && machineCredential) {
|
|
101
143
|
return { ok: false, response: privateJson({ error: "authenticated user required" }, { status: 403 }) };
|
|
102
144
|
}
|
|
145
|
+
// Every caller of this function is a human route. Agents reach pools only at
|
|
146
|
+
// `/agent/<pool>` (#837), so a `cta_` token refuses here without consulting
|
|
147
|
+
// token storage, and never falls back to a cookie, session, or Access context.
|
|
148
|
+
if (machineCredential) {
|
|
149
|
+
return {
|
|
150
|
+
ok: false,
|
|
151
|
+
response: await providerChallenge(privateJson({ error: "unauthorized" }, {
|
|
152
|
+
status: 401,
|
|
153
|
+
headers: { "WWW-Authenticate": "Bearer" },
|
|
154
|
+
}), request, baseUrl, auth),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
103
157
|
if (auth.length === 0 && !interactiveOnly && !explicit) {
|
|
104
158
|
const actor = { kind: "anonymous" };
|
|
105
159
|
const identity = { actor, interactive: false };
|
|
106
|
-
|
|
107
|
-
try {
|
|
108
|
-
access = parseConnectorAccess(identityConfig?.connectorAccess ? await identityConfig.connectorAccess(identity) : "all", { allowReadOnly: true });
|
|
109
|
-
}
|
|
110
|
-
catch {
|
|
111
|
-
return {
|
|
112
|
-
ok: false,
|
|
113
|
-
response: privateJson({ error: "identity access resolution failed" }, { status: 403 }),
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
return {
|
|
117
|
-
ok: true,
|
|
118
|
-
actor,
|
|
119
|
-
identity,
|
|
120
|
-
...access,
|
|
121
|
-
operator: false,
|
|
122
|
-
accessTokenManagement: false,
|
|
123
|
-
credentialAdministration: "none",
|
|
124
|
-
personalConnection: "none",
|
|
125
|
-
};
|
|
160
|
+
return { ok: true, actor, identity };
|
|
126
161
|
}
|
|
162
|
+
// The built-in agent adapter answers only agent endpoints. A deployment
|
|
163
|
+
// whose only provider is that adapter still refuses here, never opens.
|
|
164
|
+
const humanAuth = auth.filter((provider) => !assertsAgents(provider));
|
|
127
165
|
let recognized;
|
|
128
166
|
try {
|
|
129
|
-
recognized = recognizedProvider(request,
|
|
167
|
+
recognized = recognizedProvider(request, humanAuth, runtimeContext);
|
|
130
168
|
}
|
|
131
169
|
catch {
|
|
132
170
|
return { ok: false, response: privateJson({ error: "credential recognition failed" }, { status: 403 }) };
|
|
133
171
|
}
|
|
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
172
|
if (interactiveOnly && recognized && !recognized.interactiveOperator) {
|
|
144
173
|
return { ok: false, response: privateJson({ error: "authenticated user required" }, { status: 403 }) };
|
|
145
174
|
}
|
|
146
175
|
let lastResponse = null;
|
|
147
|
-
const eligible =
|
|
148
|
-
(!explicit ||
|
|
176
|
+
const eligible = humanAuth.filter((provider) => (!interactiveOnly || provider.interactiveOperator) &&
|
|
177
|
+
(!explicit || provider.kind !== "access_token" || !provider.recognizesCredential));
|
|
149
178
|
// A header owns one verdict even for adapters without a recognition hook.
|
|
150
179
|
const candidates = recognized ? [recognized] : explicit ? eligible.slice(0, 1) : eligible;
|
|
151
180
|
for (const provider of candidates) {
|
|
152
181
|
const result = await provider.authorize(request, baseUrl, runtimeContext);
|
|
182
|
+
// Only the built-in access-token adapter asserts an agent, and it never
|
|
183
|
+
// answers here. Any `agent` claim refuses like an unknown token.
|
|
184
|
+
const agent = result.ok ? result.agent : undefined;
|
|
185
|
+
if (result.ok && agent !== undefined) {
|
|
186
|
+
lastResponse = privateJson({ error: "unauthorized" }, {
|
|
187
|
+
status: 401,
|
|
188
|
+
headers: { "WWW-Authenticate": "Bearer" },
|
|
189
|
+
});
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
153
192
|
if (result.ok) {
|
|
154
193
|
if (interactiveOnly && !result.userId) {
|
|
155
194
|
return { ok: false, response: privateJson({ error: "authenticated user required" }, { status: 403 }) };
|
|
@@ -164,7 +203,7 @@ export async function authorize(request, baseUrl, auth, runtimeContext, identity
|
|
|
164
203
|
const interactive = Boolean(result.userId && provider.interactiveOperator);
|
|
165
204
|
// Principal-only API callers still need a stable typed actor for history
|
|
166
205
|
// ownership. Never collapse distinct personal partitions into { kind }.
|
|
167
|
-
const actorId = subjectId ?? principal?.id;
|
|
206
|
+
const actorId = agent ?? subjectId ?? principal?.id;
|
|
168
207
|
const recordNamespace = actorNamespace ?? (subjectId === undefined ? principal?.namespace : undefined);
|
|
169
208
|
const actor = {
|
|
170
209
|
kind: provider.kind,
|
|
@@ -175,48 +214,9 @@ export async function authorize(request, baseUrl, auth, runtimeContext, identity
|
|
|
175
214
|
actor,
|
|
176
215
|
...(subject ? { subject } : {}),
|
|
177
216
|
...(principal ? { principal } : {}),
|
|
217
|
+
...(agent !== undefined ? { agent } : {}),
|
|
178
218
|
interactive,
|
|
179
219
|
};
|
|
180
|
-
let operator = interactive;
|
|
181
|
-
let accessTokenManagement = false;
|
|
182
|
-
let credentialAdministration = "none";
|
|
183
|
-
let personalConnection = "none";
|
|
184
|
-
let access;
|
|
185
|
-
try {
|
|
186
|
-
if (identityConfig?.activityAccess) {
|
|
187
|
-
operator = principal ? await identityConfig.activityAccess(principal) : false;
|
|
188
|
-
}
|
|
189
|
-
access = parseConnectorAccess(identityConfig?.connectorAccess ? await identityConfig.connectorAccess(identity) : "all", { allowReadOnly: true });
|
|
190
|
-
if (interactive) {
|
|
191
|
-
accessTokenManagement = identityConfig?.accessTokenManagement
|
|
192
|
-
? await identityConfig.accessTokenManagement(identity)
|
|
193
|
-
: false;
|
|
194
|
-
if (typeof accessTokenManagement !== "boolean")
|
|
195
|
-
throw new Error("invalid token management permission");
|
|
196
|
-
credentialAdministration = identityConfig?.credentialAdministration
|
|
197
|
-
? await identityConfig.credentialAdministration(identity)
|
|
198
|
-
: "none";
|
|
199
|
-
personalConnection =
|
|
200
|
-
principal && identityConfig?.personalConnection
|
|
201
|
-
? await identityConfig.personalConnection(identity)
|
|
202
|
-
: "none";
|
|
203
|
-
}
|
|
204
|
-
if (typeof operator !== "boolean")
|
|
205
|
-
throw new Error("invalid activity permission");
|
|
206
|
-
for (const permission of [credentialAdministration, personalConnection]) {
|
|
207
|
-
if (permission !== "all" &&
|
|
208
|
-
permission !== "none" &&
|
|
209
|
-
(!Array.isArray(permission) ||
|
|
210
|
-
!permission.every((id) => typeof id === "string" && /^[a-z0-9_-]+$/.test(id))))
|
|
211
|
-
throw new Error("invalid identity permission");
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
catch {
|
|
215
|
-
return {
|
|
216
|
-
ok: false,
|
|
217
|
-
response: privateJson({ error: "identity access resolution failed" }, { status: 403 }),
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
220
|
return {
|
|
221
221
|
ok: true,
|
|
222
222
|
actor,
|
|
@@ -224,12 +224,6 @@ export async function authorize(request, baseUrl, auth, runtimeContext, identity
|
|
|
224
224
|
...(result.sessionCookies?.length ? { sessionCookies: result.sessionCookies } : {}),
|
|
225
225
|
...(subject && partitionIdentity ? { subjectKey: await identityStorageKey(subject) } : {}),
|
|
226
226
|
...(principal && partitionIdentity ? { principalKey: await identityStorageKey(principal) } : {}),
|
|
227
|
-
...access,
|
|
228
|
-
accessTokenManagement,
|
|
229
|
-
credentialAdministration,
|
|
230
|
-
personalConnection,
|
|
231
|
-
operator,
|
|
232
|
-
...(interactive && operator ? { uiAdminEligible: true } : {}),
|
|
233
227
|
};
|
|
234
228
|
}
|
|
235
229
|
lastResponse = result.response;
|
|
@@ -245,15 +239,57 @@ export async function authorize(request, baseUrl, auth, runtimeContext, identity
|
|
|
245
239
|
}), request, baseUrl, auth, explicit ? candidates[0] : undefined),
|
|
246
240
|
};
|
|
247
241
|
}
|
|
242
|
+
/** The machine-only challenge: Bearer, with no authorization server to discover. */
|
|
243
|
+
function agentChallenge() {
|
|
244
|
+
return {
|
|
245
|
+
ok: false,
|
|
246
|
+
response: privateJson({ error: "unauthorized" }, { status: 401, headers: { "WWW-Authenticate": "Bearer" } }),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Admission at an agent endpoint, `/agent/<pool>` (#837). Only the built-in
|
|
251
|
+
* `cta_` adapter is consulted: Clerk and Access never run, cookies and Clerk
|
|
252
|
+
* handshake parameters are stripped, and the runtime context, which carries
|
|
253
|
+
* Access's `ctx.access` even on a bypassed path, is never passed. Anything but
|
|
254
|
+
* an admitted declared agent gets the machine-only challenge.
|
|
255
|
+
*/
|
|
256
|
+
export async function authorizeAgent(request, baseUrl, auth) {
|
|
257
|
+
const provider = auth.find(assertsAgents);
|
|
258
|
+
if (!provider || !isMachineCredential(request))
|
|
259
|
+
return agentChallenge();
|
|
260
|
+
let result;
|
|
261
|
+
try {
|
|
262
|
+
result = await provider.authorize(authorizationRequest(request), baseUrl, undefined);
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return agentChallenge();
|
|
266
|
+
}
|
|
267
|
+
if (!result.ok) {
|
|
268
|
+
void result.response.body?.cancel().catch(() => { });
|
|
269
|
+
return agentChallenge();
|
|
270
|
+
}
|
|
271
|
+
const { agent } = result;
|
|
272
|
+
if (!isAgentName(agent) || result.userId !== undefined || result.principal !== undefined)
|
|
273
|
+
return agentChallenge();
|
|
274
|
+
const namespace = activityActorNamespace(provider);
|
|
275
|
+
const subject = { namespace: namespace ?? `connecta:auth:${provider.kind}`, id: result.subjectId ?? agent };
|
|
276
|
+
const actor = { kind: provider.kind, id: agent, ...(namespace ? { namespace } : {}) };
|
|
277
|
+
return {
|
|
278
|
+
ok: true,
|
|
279
|
+
actor,
|
|
280
|
+
identity: { actor, subject, agent, interactive: false },
|
|
281
|
+
subjectKey: await identityStorageKey(subject),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
248
284
|
/** Human routes reject machine credentials before any verification or storage I/O. */
|
|
249
|
-
export async function authorizeUiIdentity(request, baseUrl, auth, purpose, runtimeContext
|
|
285
|
+
export async function authorizeUiIdentity(request, baseUrl, auth, purpose, runtimeContext) {
|
|
250
286
|
if (!auth.some((provider) => provider.interactiveOperator)) {
|
|
251
287
|
return {
|
|
252
288
|
ok: false,
|
|
253
289
|
response: privateJson({ error: `${purpose} requires interactive user authentication` }, { status: 403 }),
|
|
254
290
|
};
|
|
255
291
|
}
|
|
256
|
-
return authorize(request, baseUrl, auth, runtimeContext,
|
|
292
|
+
return authorize(request, baseUrl, auth, runtimeContext, true, true);
|
|
257
293
|
}
|
|
258
294
|
export function isSameOrigin(request, baseUrl) {
|
|
259
295
|
const origin = request.headers.get("origin");
|
|
@@ -281,24 +317,79 @@ export function withSecurityHeaders(response, requestUrl, _path) {
|
|
|
281
317
|
headers,
|
|
282
318
|
});
|
|
283
319
|
}
|
|
284
|
-
/**
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
320
|
+
/**
|
|
321
|
+
* Plain `/mcp` and every operator route admit a maintainer (#837). An open
|
|
322
|
+
* deployment has no inbound auth and so no maintainers: its one anonymous
|
|
323
|
+
* caller keeps the whole default endpoint, as the startup warning says.
|
|
324
|
+
*/
|
|
325
|
+
export function admitsMaintainer(opts, authz) {
|
|
326
|
+
return opts.config.auth.length === 0 || isMaintainer(opts.maintainers, authz.identity);
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* The declared pool `/mcp/<name>` serves to this person, or undefined for the
|
|
330
|
+
* same 404. An agent is never admitted here, even when listed (#837).
|
|
331
|
+
*/
|
|
332
|
+
export function memberPool(opts, name, identity) {
|
|
333
|
+
if (identity.agent !== undefined)
|
|
334
|
+
return undefined;
|
|
335
|
+
const pool = opts.pools.get(name);
|
|
336
|
+
return pool?.members.some((member) => isMember(member, identity)) ? pool : undefined;
|
|
337
|
+
}
|
|
338
|
+
/** The declared pool `/agent/<name>` serves to this agent, or undefined for the same 404. */
|
|
339
|
+
export function agentPool(opts, name, identity) {
|
|
340
|
+
if (identity.agent === undefined)
|
|
341
|
+
return undefined;
|
|
342
|
+
const pool = opts.pools.get(name);
|
|
343
|
+
return pool?.members.some((member) => member.kind === "agent" && isMember(member, identity)) ? pool : undefined;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* The connectors of every pool this identity is a member of (#837): what a
|
|
347
|
+
* pool member may ever manage, and only their personal ones among these.
|
|
348
|
+
*/
|
|
349
|
+
export function memberConnectors(opts, identity) {
|
|
350
|
+
const ids = new Set();
|
|
351
|
+
for (const name of opts.pools.keys()) {
|
|
352
|
+
const pool = memberPool(opts, name, identity);
|
|
353
|
+
if (!pool)
|
|
288
354
|
continue;
|
|
289
|
-
if (
|
|
290
|
-
|
|
291
|
-
|
|
355
|
+
if (pool.access.connectorIds === "all")
|
|
356
|
+
return "all";
|
|
357
|
+
for (const id of pool.access.connectorIds)
|
|
358
|
+
ids.add(id);
|
|
292
359
|
}
|
|
360
|
+
return ids;
|
|
293
361
|
}
|
|
294
|
-
|
|
295
|
-
|
|
362
|
+
/**
|
|
363
|
+
* Whether this identity may start, finish, or remove a connector's auth
|
|
364
|
+
* (#837). Only an interactive person ever may; an agent or any other machine
|
|
365
|
+
* credential manages nothing. Shared auth is deployment-wide, so it takes a
|
|
366
|
+
* maintainer. A personal connector takes a stable principal that is a
|
|
367
|
+
* maintainer, whose plain `/mcp` includes it, or a member of a pool that
|
|
368
|
+
* contains it; either way it is always the caller's own partition, which
|
|
369
|
+
* callers bind by principal. Every path that issues a link, starts,
|
|
370
|
+
* completes, or disconnects auth, or writes a credential, asks this.
|
|
371
|
+
*/
|
|
372
|
+
export function mayManageConnector(opts, authz, connector) {
|
|
373
|
+
const identity = authz.identity;
|
|
374
|
+
if (!identity.interactive || identity.agent !== undefined || !identity.principal)
|
|
296
375
|
return false;
|
|
297
|
-
if (
|
|
376
|
+
if (isMaintainer(opts.maintainers, identity))
|
|
377
|
+
return true;
|
|
378
|
+
if (connector.authScope !== "personal")
|
|
298
379
|
return false;
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
380
|
+
const pools = memberConnectors(opts, identity);
|
|
381
|
+
return pools === "all" || pools.has(connector.id);
|
|
382
|
+
}
|
|
383
|
+
/** Agent token list, issue, rotate, and revoke: an interactive maintainer only (#837). */
|
|
384
|
+
export function managesAgentTokens(opts, authz) {
|
|
385
|
+
return (authz.identity.interactive && authz.identity.agent === undefined && isMaintainer(opts.maintainers, authz.identity));
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Activity reads: a maintainer principal, interactive or not (#837). Members
|
|
389
|
+
* and agents read none, and an open deployment has no maintainer.
|
|
390
|
+
*/
|
|
391
|
+
export function readsActivity(opts, authz) {
|
|
392
|
+
return isMaintainer(opts.maintainers, authz.identity);
|
|
302
393
|
}
|
|
303
394
|
/** Preserve refreshed browser cookies on the redirect or completion page. */
|
|
304
395
|
export function withSessionCookies(response, cookies) {
|