@zackbart/connecta 0.22.3 → 0.24.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/AGENTS.md +5 -0
- package/CHANGELOG.md +82 -0
- package/README.md +19 -11
- package/dist/activity-friction.d.ts +3 -0
- package/dist/activity-friction.js +19 -0
- package/dist/activity.d.ts +11 -2
- package/dist/activity.js +15 -19
- package/dist/auth/downstream-oauth.d.ts +2 -1
- package/dist/auth/downstream-oauth.js +10 -1
- package/dist/branding.d.ts +67 -0
- package/dist/branding.js +176 -0
- package/dist/catalog-service.d.ts +1 -6
- package/dist/catalog-service.js +3 -53
- package/dist/connectors/remote-mcp.js +3 -5
- package/dist/credential-contract.d.ts +24 -0
- package/dist/credential-contract.js +1 -0
- package/dist/credential-rules.d.ts +85 -0
- package/dist/credential-rules.js +107 -0
- package/dist/credentials.d.ts +4 -100
- package/dist/credentials.js +3 -107
- package/dist/errors.d.ts +1 -1
- package/dist/execute.d.ts +5 -52
- package/dist/execute.js +40 -347
- package/dist/executors/quickjs-protocol.d.ts +0 -7
- package/dist/executors/quickjs-protocol.js +2 -10
- package/dist/executors/quickjs.js +1 -1
- package/dist/index.d.ts +23 -56
- package/dist/index.js +30 -58
- package/dist/invocation.d.ts +0 -33
- package/dist/invocation.js +56 -124
- package/dist/meta-tools.d.ts +7 -6
- package/dist/meta-tools.js +14 -21
- package/dist/module-contracts.d.ts +19 -0
- package/dist/module-contracts.js +1 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +6 -3
- package/dist/operator-ui/view.d.ts +2 -18
- package/dist/operator-ui/view.js +3 -20
- package/dist/registry.d.ts +4 -1
- package/dist/registry.js +8 -6
- package/dist/routes/activity.js +1 -1
- package/dist/routes/credentials.js +5 -2
- package/dist/routes/mcp.js +7 -51
- package/dist/routes/oauth-management.d.ts +2 -0
- package/dist/routes/oauth-management.js +108 -0
- package/dist/routes/oauth.d.ts +0 -1
- package/dist/routes/oauth.js +21 -121
- package/dist/routes/shared.d.ts +19 -17
- package/dist/routes/shared.js +48 -44
- package/dist/routes/ui.js +36 -33
- package/dist/server.d.ts +1 -2
- package/dist/server.js +7 -45
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +55 -19
- package/dist/types.d.ts +3 -15
- package/dist/ui.d.ts +15 -70
- package/dist/ui.js +176 -317
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +31 -26
- package/documentation/auth.md +65 -111
- package/documentation/call-admission.md +10 -11
- package/documentation/cloudflare.md +1 -1
- package/documentation/code-mode.md +65 -260
- package/documentation/connectors.md +8 -6
- package/documentation/linear.md +1 -1
- package/documentation/meta-tools.md +37 -18
- package/documentation/mixpanel.md +1 -1
- package/documentation/notion.md +2 -2
- package/documentation/operations.md +19 -23
- package/documentation/operator-ui.md +82 -104
- package/documentation/optional-modules-upgrade.md +243 -0
- package/documentation/provider-conventions.md +7 -5
- package/documentation/revenuecat.md +1 -1
- package/documentation/storage-and-credentials.md +59 -40
- package/documentation/stripe.md +1 -1
- package/documentation/upgrading.md +83 -8
- package/ethos.md +36 -43
- package/examples/worker/AGENTS.md +3 -1
- package/examples/worker/README.md +68 -84
- package/examples/worker/src/d1-activity.ts +1 -1
- package/examples/worker/src/index.ts +11 -6
- package/package.json +18 -2
- package/templates/node/AGENTS.md +8 -6
- package/templates/node/README.md +56 -67
- package/templates/node/package.json +1 -1
- package/templates/node/src/file-activity.ts +1 -1
- package/templates/node/src/index.ts +11 -12
- package/dist/access-tokens.d.ts +0 -31
- package/dist/access-tokens.js +0 -236
- package/dist/apps-shell.d.ts +0 -37
- package/dist/apps-shell.js +0 -174
- package/dist/routes/access-tokens.d.ts +0 -6
- package/dist/routes/access-tokens.js +0 -83
package/dist/routes/shared.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { Implementation } from "@modelcontextprotocol/server";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ActivityModule, OperatorSurface } from "../module-contracts.js";
|
|
3
3
|
import type { ActivityActor, ActivityReadGate, ActivityStore } from "../activity.js";
|
|
4
|
-
import type { CredentialVault } from "../
|
|
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
7
|
import type { Registry } from "../registry.js";
|
|
8
|
-
import type { AuthenticatedIdentity, ConnectaBranding, Executor,
|
|
9
|
-
import type { ConnectaIdentityConfig } from "../index.js";
|
|
8
|
+
import type { AuthenticatedIdentity, ConnectaBranding, Executor, InboundAuth, InboundAuthRuntimeContext, Logger } from "../types.js";
|
|
9
|
+
import type { ConnectorPermission, ConnectaIdentityConfig } from "../index.js";
|
|
10
10
|
export { msg } from "../errors.js";
|
|
11
11
|
export interface ServerOptions {
|
|
12
12
|
registry: Registry;
|
|
@@ -37,8 +37,9 @@ export interface ServerOptions {
|
|
|
37
37
|
requestAdmission: AdmissionController;
|
|
38
38
|
/** Encrypted connector-credential storage backing the Credentials page. */
|
|
39
39
|
credentialVault?: CredentialVault | undefined;
|
|
40
|
-
/**
|
|
41
|
-
|
|
40
|
+
/** Optional browser routes, with no implementation import in core. */
|
|
41
|
+
ui?: OperatorSurface | undefined;
|
|
42
|
+
activityModule?: ActivityModule | undefined;
|
|
42
43
|
/** Optional browser UI and OAuth result-page labels. */
|
|
43
44
|
branding?: ConnectaBranding | undefined;
|
|
44
45
|
}
|
|
@@ -74,22 +75,14 @@ export declare function authorize(request: Request, baseUrl: string, auth: Inbou
|
|
|
74
75
|
principalKey?: string;
|
|
75
76
|
connectorIds: "all" | readonly string[];
|
|
76
77
|
operator: boolean;
|
|
78
|
+
credentialAdministration: ConnectorPermission;
|
|
79
|
+
personalConnection: ConnectorPermission;
|
|
77
80
|
/** Backward-compatible name used by operator views. */
|
|
78
81
|
uiAdminEligible?: boolean;
|
|
79
82
|
} | {
|
|
80
83
|
ok: false;
|
|
81
84
|
response: Response;
|
|
82
85
|
}>;
|
|
83
|
-
export declare function authorizeUiAdmin(request: Request, baseUrl: string, auth: InboundAuth[], purpose?: string, runtimeContext?: RuntimeExecutionContext, identityConfig?: ConnectaIdentityConfig): Promise<{
|
|
84
|
-
ok: true;
|
|
85
|
-
userId: string;
|
|
86
|
-
principal?: IdentityReference;
|
|
87
|
-
principalKey?: string;
|
|
88
|
-
connectorIds: "all" | readonly string[];
|
|
89
|
-
} | {
|
|
90
|
-
ok: false;
|
|
91
|
-
response: Response;
|
|
92
|
-
}>;
|
|
93
86
|
export declare function authorizeUiIdentity(request: Request, baseUrl: string, auth: InboundAuth[], purpose: string, runtimeContext?: RuntimeExecutionContext, identityConfig?: ConnectaIdentityConfig): Promise<Extract<Awaited<ReturnType<typeof authorize>>, {
|
|
94
87
|
ok: true;
|
|
95
88
|
}> | {
|
|
@@ -97,4 +90,13 @@ export declare function authorizeUiIdentity(request: Request, baseUrl: string, a
|
|
|
97
90
|
response: Response;
|
|
98
91
|
}>;
|
|
99
92
|
export declare function isSameOrigin(request: Request, baseUrl: string): boolean;
|
|
100
|
-
export declare function withSecurityHeaders(response: Response, requestUrl: URL,
|
|
93
|
+
export declare function withSecurityHeaders(response: Response, requestUrl: URL, _path: string): Response;
|
|
94
|
+
type AuthorizedIdentity = Extract<Awaited<ReturnType<typeof authorize>>, {
|
|
95
|
+
ok: true;
|
|
96
|
+
}>;
|
|
97
|
+
/** Unknown configured ids refuse the complete view, including management rights. */
|
|
98
|
+
export declare function validateAuthPermissions(authz: AuthorizedIdentity, registry: Registry): void;
|
|
99
|
+
export declare function mayManageConnector(authz: AuthorizedIdentity, connector: {
|
|
100
|
+
id: string;
|
|
101
|
+
authScope?: "shared" | "personal";
|
|
102
|
+
}): boolean;
|
package/dist/routes/shared.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { identityStorageKey, validIdentityReference } from "../identity.js";
|
|
2
|
-
import { operatorPageForPath } from "../ui.js";
|
|
3
2
|
export { msg } from "../errors.js";
|
|
4
3
|
export function privateJson(body, init = {}) {
|
|
5
4
|
const headers = new Headers(init.headers);
|
|
@@ -36,7 +35,9 @@ export async function authorize(request, baseUrl, auth, runtimeContext, identity
|
|
|
36
35
|
const identity = { actor, interactive: false };
|
|
37
36
|
let connectorIds = "all";
|
|
38
37
|
try {
|
|
39
|
-
connectorIds = await identityConfig
|
|
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");
|
|
40
41
|
}
|
|
41
42
|
catch {
|
|
42
43
|
return {
|
|
@@ -44,7 +45,7 @@ export async function authorize(request, baseUrl, auth, runtimeContext, identity
|
|
|
44
45
|
response: privateJson({ error: "identity access resolution failed" }, { status: 403 }),
|
|
45
46
|
};
|
|
46
47
|
}
|
|
47
|
-
return { ok: true, actor, identity, connectorIds, operator: false };
|
|
48
|
+
return { ok: true, actor, identity, connectorIds, operator: false, credentialAdministration: "none", personalConnection: "none" };
|
|
48
49
|
}
|
|
49
50
|
let lastResponse = null;
|
|
50
51
|
for (const provider of auth) {
|
|
@@ -74,14 +75,26 @@ export async function authorize(request, baseUrl, auth, runtimeContext, identity
|
|
|
74
75
|
interactive,
|
|
75
76
|
};
|
|
76
77
|
let operator = interactive;
|
|
78
|
+
let credentialAdministration = "none";
|
|
79
|
+
let personalConnection = "none";
|
|
77
80
|
let connectorIds = "all";
|
|
78
81
|
try {
|
|
79
|
-
if (identityConfig?.
|
|
82
|
+
if (identityConfig?.activityAccess) {
|
|
80
83
|
operator = interactive && principal
|
|
81
|
-
? await identityConfig.
|
|
84
|
+
? await identityConfig.activityAccess(principal)
|
|
82
85
|
: false;
|
|
83
86
|
}
|
|
84
|
-
connectorIds = await identityConfig
|
|
87
|
+
connectorIds = identityConfig?.connectorAccess ? await identityConfig.connectorAccess(identity) : "all";
|
|
88
|
+
if (interactive) {
|
|
89
|
+
credentialAdministration = identityConfig?.credentialAdministration ? await identityConfig.credentialAdministration(identity) : "none";
|
|
90
|
+
personalConnection = principal && identityConfig?.personalConnection ? await identityConfig.personalConnection(identity) : "none";
|
|
91
|
+
}
|
|
92
|
+
if (typeof operator !== "boolean")
|
|
93
|
+
throw new Error("invalid activity permission");
|
|
94
|
+
for (const permission of [connectorIds, credentialAdministration, personalConnection]) {
|
|
95
|
+
if (permission !== "all" && permission !== "none" && (!Array.isArray(permission) || !permission.every(id => typeof id === "string" && /^[a-z0-9_-]+$/.test(id))))
|
|
96
|
+
throw new Error("invalid identity permission");
|
|
97
|
+
}
|
|
85
98
|
}
|
|
86
99
|
catch {
|
|
87
100
|
return {
|
|
@@ -100,6 +113,8 @@ export async function authorize(request, baseUrl, auth, runtimeContext, identity
|
|
|
100
113
|
? { principalKey: await identityStorageKey(principal) }
|
|
101
114
|
: {}),
|
|
102
115
|
connectorIds,
|
|
116
|
+
credentialAdministration,
|
|
117
|
+
personalConnection,
|
|
103
118
|
operator,
|
|
104
119
|
...(operator ? { uiAdminEligible: true } : {}),
|
|
105
120
|
};
|
|
@@ -118,34 +133,6 @@ export async function authorize(request, baseUrl, auth, runtimeContext, identity
|
|
|
118
133
|
}),
|
|
119
134
|
};
|
|
120
135
|
}
|
|
121
|
-
export async function authorizeUiAdmin(request, baseUrl, auth, purpose = "credential management", runtimeContext, identityConfig) {
|
|
122
|
-
// Operator mutation is intentionally narrower than /mcp and /ui/data: only
|
|
123
|
-
// an interactive provider may admit it. A static bearer token is useful
|
|
124
|
-
// for headless tool calls but must not become a deployment-admin key.
|
|
125
|
-
//
|
|
126
|
-
// Every interactive provider gets a turn, the way the /mcp gate does.
|
|
127
|
-
// Stopping at the first would make admission depend on config order: a failed gate or
|
|
128
|
-
// missing user may simply mean a later provider is the one meant to admit.
|
|
129
|
-
// The last refusal is returned if none do.
|
|
130
|
-
const authz = await authorizeUiIdentity(request, baseUrl, auth, purpose, runtimeContext, identityConfig);
|
|
131
|
-
if (!authz.ok)
|
|
132
|
-
return authz;
|
|
133
|
-
if (!authz.operator || !authz.actor.id) {
|
|
134
|
-
return {
|
|
135
|
-
ok: false,
|
|
136
|
-
response: privateJson({ error: `${purpose} requires operator access` }, { status: 403 }),
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
return {
|
|
140
|
-
ok: true,
|
|
141
|
-
userId: authz.identity.principal?.id ?? authz.actor.id,
|
|
142
|
-
...(authz.identity.principal
|
|
143
|
-
? { principal: authz.identity.principal }
|
|
144
|
-
: {}),
|
|
145
|
-
...(authz.principalKey ? { principalKey: authz.principalKey } : {}),
|
|
146
|
-
connectorIds: authz.connectorIds,
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
136
|
export async function authorizeUiIdentity(request, baseUrl, auth, purpose, runtimeContext, identityConfig) {
|
|
150
137
|
const providers = auth.filter((candidate) => candidate.interactiveOperator);
|
|
151
138
|
if (providers.length === 0) {
|
|
@@ -181,25 +168,42 @@ export function isSameOrigin(request, baseUrl) {
|
|
|
181
168
|
return false;
|
|
182
169
|
}
|
|
183
170
|
}
|
|
184
|
-
export function withSecurityHeaders(response, requestUrl,
|
|
171
|
+
export function withSecurityHeaders(response, requestUrl, _path) {
|
|
185
172
|
const headers = new Headers(response.headers);
|
|
186
173
|
headers.set("X-Content-Type-Options", "nosniff");
|
|
187
174
|
headers.set("Referrer-Policy", "no-referrer");
|
|
188
175
|
if (requestUrl.protocol === "https:") {
|
|
189
176
|
headers.set("Strict-Transport-Security", "max-age=31536000");
|
|
190
177
|
}
|
|
191
|
-
if (operatorPageForPath(path) || path === "/ui") {
|
|
192
|
-
// Operator HTML responses ship their own nonce-based script CSP (which
|
|
193
|
-
// already includes frame-ancestors 'none'); only fall back to the
|
|
194
|
-
// framing-only directive when no CSP is present (for example redirects).
|
|
195
|
-
if (!headers.has("Content-Security-Policy")) {
|
|
196
|
-
headers.set("Content-Security-Policy", "frame-ancestors 'none'");
|
|
197
|
-
}
|
|
198
|
-
headers.set("X-Frame-Options", "DENY");
|
|
199
|
-
}
|
|
200
178
|
return new Response(response.body, {
|
|
201
179
|
status: response.status,
|
|
202
180
|
statusText: response.statusText,
|
|
203
181
|
headers,
|
|
204
182
|
});
|
|
205
183
|
}
|
|
184
|
+
/** Unknown configured ids refuse the complete view, including management rights. */
|
|
185
|
+
export function validateAuthPermissions(authz, registry) {
|
|
186
|
+
for (const value of [
|
|
187
|
+
authz.connectorIds,
|
|
188
|
+
authz.credentialAdministration,
|
|
189
|
+
authz.personalConnection,
|
|
190
|
+
]) {
|
|
191
|
+
if (value === "all" || value === "none")
|
|
192
|
+
continue;
|
|
193
|
+
if (!Array.isArray(value) || value.some(id => !registry.getConnector(id))) {
|
|
194
|
+
throw new Error("invalid identity permission connector ids");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
export function mayManageConnector(authz, connector) {
|
|
199
|
+
if (!authz.identity.interactive)
|
|
200
|
+
return false;
|
|
201
|
+
if (authz.connectorIds !== "all" && !authz.connectorIds.includes(connector.id)) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
const permission = connector.authScope === "personal"
|
|
205
|
+
? authz.personalConnection
|
|
206
|
+
: authz.credentialAdministration;
|
|
207
|
+
return permission === "all" ||
|
|
208
|
+
(permission !== "none" && permission.includes(connector.id));
|
|
209
|
+
}
|
package/dist/routes/ui.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { CONNECTA_VERSION } from "../version.js";
|
|
1
2
|
import { CONNECTA_FAVICON_ICO } from "../favicon.js";
|
|
2
|
-
import { buildUiData, CONNECTA_FAVICON_SVG,
|
|
3
|
-
import { authorize, msg, privateJson, } from "./shared.js";
|
|
3
|
+
import { buildUiData, CONNECTA_FAVICON_SVG, operatorPageForPath, renderUiHtml, } from "../ui.js";
|
|
4
|
+
import { authorize, mayManageConnector, validateAuthPermissions, msg, privateJson, } from "./shared.js";
|
|
4
5
|
/**
|
|
5
6
|
* Headers that make an operator-supplied favicon body inert on this origin.
|
|
6
7
|
* The SVG route is the sharp one: `image/svg+xml` is an *active* content type,
|
|
@@ -67,7 +68,7 @@ export async function routeUi(context) {
|
|
|
67
68
|
});
|
|
68
69
|
}
|
|
69
70
|
const operatorPage = operatorPageForPath(path);
|
|
70
|
-
if (operatorPage) {
|
|
71
|
+
if (operatorPage && (operatorPage !== "activity" || opts.activity?.list)) {
|
|
71
72
|
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
72
73
|
return privateJson({ error: "method not allowed" }, { status: 405 });
|
|
73
74
|
}
|
|
@@ -97,13 +98,17 @@ export async function routeUi(context) {
|
|
|
97
98
|
},
|
|
98
99
|
});
|
|
99
100
|
}
|
|
100
|
-
|
|
101
|
+
const detail = /^\/ui\/connectors\/([a-z0-9_-]+)$/.exec(path);
|
|
102
|
+
if (path !== "/ui/data" && !detail)
|
|
101
103
|
return null;
|
|
104
|
+
if (request.method !== "GET")
|
|
105
|
+
return privateJson({ error: "method not allowed" }, { status: 405 });
|
|
102
106
|
const authz = await authorize(request, baseUrl, opts.auth, runtimeContext, opts.identity);
|
|
103
107
|
if (!authz.ok)
|
|
104
108
|
return authz.response;
|
|
105
109
|
let registry;
|
|
106
110
|
try {
|
|
111
|
+
validateAuthPermissions(authz, opts.registry);
|
|
107
112
|
registry = opts.registry.scoped({
|
|
108
113
|
connectorIds: authz.connectorIds,
|
|
109
114
|
...(authz.subjectKey ? { subjectKey: authz.subjectKey } : {}),
|
|
@@ -113,33 +118,31 @@ export async function routeUi(context) {
|
|
|
113
118
|
catch (error) {
|
|
114
119
|
return privateJson({ error: msg(error) }, { status: 403 });
|
|
115
120
|
}
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
: opts.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
(!opts.identity?.operatorAccess || eligibleOperator), credentialManagement, defer, interactiveManager, opts.discoveryConcurrency, accessTokenManagement, personalManager ? authz.principalKey : undefined);
|
|
144
|
-
return privateJson(data);
|
|
121
|
+
const visible = registry.listConnectors();
|
|
122
|
+
const mayManage = (id) => { const connector = registry.getConnector(id); return Boolean(connector && mayManageConnector(authz, connector)); };
|
|
123
|
+
const permissions = (connector) => ({
|
|
124
|
+
use: true,
|
|
125
|
+
manageSharedAuth: connector.authScope !== "personal" && mayManage(connector.id),
|
|
126
|
+
connectPersonal: connector.authScope === "personal" && mayManage(connector.id),
|
|
127
|
+
});
|
|
128
|
+
const activityEnabled = Boolean(opts.activity?.list) && authz.operator;
|
|
129
|
+
const credentialManagement = visible.some(c => c.credential && mayManage(c.id))
|
|
130
|
+
? opts.credentialVault ? "available" : "vault_not_configured"
|
|
131
|
+
: authz.identity.interactive && !visible.some(c => c.credential) ? "no_slots" : "requires_operator";
|
|
132
|
+
if (detail) {
|
|
133
|
+
const connector = registry.getConnector(detail[1]);
|
|
134
|
+
if (!connector)
|
|
135
|
+
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 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
|
+
return privateJson({ ...data.connectors[0], permissions: permissions(connector) });
|
|
139
|
+
}
|
|
140
|
+
return privateJson({
|
|
141
|
+
serverInfo: opts.serverInfo,
|
|
142
|
+
connectaVersion: CONNECTA_VERSION,
|
|
143
|
+
activityEnabled,
|
|
144
|
+
credentialManagement,
|
|
145
|
+
oauthManagement: visible.some(c => mayManage(c.id)),
|
|
146
|
+
connectors: visible.map(c => ({ id: c.id, ...(c.title ? { title: c.title } : {}), ...(c.description ? { description: c.description } : {}), authScope: c.authScope ?? "shared", status: "loading", toolCount: 0, tools: [], oauth: Boolean(c.startAuth && c.disconnectAuth), permissions: permissions(c) })),
|
|
147
|
+
});
|
|
145
148
|
}
|
package/dist/server.d.ts
CHANGED
|
@@ -4,7 +4,6 @@ export type { ServerOptions } from "./routes/shared.js";
|
|
|
4
4
|
* Build the Web-standard fetch handler.
|
|
5
5
|
*
|
|
6
6
|
* Route ordering is the contract: private mutation routes precede wildcard
|
|
7
|
-
* OPTIONS,
|
|
8
|
-
* wrapper is applied to every response.
|
|
7
|
+
* OPTIONS, and the security wrapper is applied to every response, including 404s.
|
|
9
8
|
*/
|
|
10
9
|
export declare function createFetchHandler(opts: ServerOptions): (request: Request, runtimeContext?: RuntimeExecutionContext) => Promise<Response>;
|
package/dist/server.js
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
import { isAdmittingExecutor } from "./executor-admission.js";
|
|
2
|
-
import { routeAccessTokens } from "./routes/access-tokens.js";
|
|
3
|
-
import { routeActivity } from "./routes/activity.js";
|
|
4
|
-
import { routeCredentials } from "./routes/credentials.js";
|
|
5
2
|
import { createMcpRoute, MCP_CORS_HEADERS } from "./routes/mcp.js";
|
|
6
|
-
import { routeOAuthCallback,
|
|
3
|
+
import { routeOAuthCallback, } from "./routes/oauth.js";
|
|
7
4
|
import { withSecurityHeaders, } from "./routes/shared.js";
|
|
8
|
-
import { routeUi } from "./routes/ui.js";
|
|
9
5
|
/**
|
|
10
6
|
* Build the Web-standard fetch handler.
|
|
11
7
|
*
|
|
12
8
|
* Route ordering is the contract: private mutation routes precede wildcard
|
|
13
|
-
* OPTIONS,
|
|
14
|
-
* wrapper is applied to every response.
|
|
9
|
+
* OPTIONS, and the security wrapper is applied to every response, including 404s.
|
|
15
10
|
*/
|
|
16
11
|
export function createFetchHandler(opts) {
|
|
17
12
|
const { auth, publicUrl, registry } = opts;
|
|
@@ -60,15 +55,9 @@ export function createFetchHandler(opts) {
|
|
|
60
55
|
};
|
|
61
56
|
const route = async () => {
|
|
62
57
|
// Private mutations own OPTIONS so they never inherit wildcard CORS.
|
|
63
|
-
const
|
|
64
|
-
if (
|
|
65
|
-
return
|
|
66
|
-
const credentials = await routeCredentials(context);
|
|
67
|
-
if (credentials)
|
|
68
|
-
return credentials;
|
|
69
|
-
const oauthManagement = await routeOAuthManagement(context);
|
|
70
|
-
if (oauthManagement)
|
|
71
|
-
return oauthManagement;
|
|
58
|
+
const uiResponse = await opts.ui?.handle(context);
|
|
59
|
+
if (uiResponse)
|
|
60
|
+
return uiResponse;
|
|
72
61
|
if (request.method === "OPTIONS") {
|
|
73
62
|
for (const provider of auth) {
|
|
74
63
|
if (provider.handleMetadata) {
|
|
@@ -124,12 +113,8 @@ export function createFetchHandler(opts) {
|
|
|
124
113
|
},
|
|
125
114
|
reservedRoutes: [
|
|
126
115
|
"/health",
|
|
127
|
-
|
|
128
|
-
"/
|
|
129
|
-
"/tokens",
|
|
130
|
-
"/activity",
|
|
131
|
-
"/ui",
|
|
132
|
-
"/ui/*",
|
|
116
|
+
...(opts.ui?.reservedPaths ?? []),
|
|
117
|
+
...(opts.ui && opts.activity?.list ? ["/activity"] : []),
|
|
133
118
|
],
|
|
134
119
|
},
|
|
135
120
|
...(opts.deploymentInfo ? { deployment: opts.deploymentInfo } : {}),
|
|
@@ -138,32 +123,9 @@ export function createFetchHandler(opts) {
|
|
|
138
123
|
const oauthCallback = await routeOAuthCallback(context);
|
|
139
124
|
if (oauthCallback)
|
|
140
125
|
return oauthCallback;
|
|
141
|
-
const ui = await routeUi(context);
|
|
142
|
-
if (ui)
|
|
143
|
-
return ui;
|
|
144
|
-
const activity = await routeActivity(context);
|
|
145
|
-
if (activity)
|
|
146
|
-
return activity;
|
|
147
126
|
const mcp = await routeMcp(context);
|
|
148
127
|
if (mcp)
|
|
149
128
|
return mcp;
|
|
150
|
-
// Connector-owned public routes, dispatched last: a connector can add a
|
|
151
|
-
// route but never shadow one of connecta's own. A throw here is the
|
|
152
|
-
// connector's bug, not a missing route, so it surfaces as 500 rather
|
|
153
|
-
// than falling through to 404.
|
|
154
|
-
for (const connector of registry.listConnectors()) {
|
|
155
|
-
if (!connector.handleRequest)
|
|
156
|
-
continue;
|
|
157
|
-
try {
|
|
158
|
-
const response = await connector.handleRequest(request, registry.contextFor(connector.id, baseUrl));
|
|
159
|
-
if (response)
|
|
160
|
-
return response;
|
|
161
|
-
}
|
|
162
|
-
catch (error) {
|
|
163
|
-
opts.logger.error(`[connecta] connector "${connector.id}" handleRequest failed`, error);
|
|
164
|
-
return new Response("Internal Server Error", { status: 500 });
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
129
|
return new Response("Not Found", { status: 404 });
|
|
168
130
|
};
|
|
169
131
|
return withSecurityHeaders(await route(), url, path);
|
package/dist/skills.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Connector } from "./types.js";
|
|
2
|
-
export declare const CONNECTA_INSTRUCTIONS = "Choose a route before discovery. A known-address read needs only call_tool. Unknown-address read-only work starts with
|
|
2
|
+
export declare 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.";
|
|
3
3
|
/** Shared Connecta routing guidance, byte-identical across deployments. */
|
|
4
4
|
export declare const USAGE_SKILL: string;
|
|
5
5
|
/** The always-loaded MCP `instructions` string. */
|
package/dist/skills.js
CHANGED
|
@@ -1,59 +1,58 @@
|
|
|
1
|
-
export const CONNECTA_INSTRUCTIONS = 'Choose a route before discovery. A known-address read needs only call_tool. Unknown-address read-only work starts with
|
|
1
|
+
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
2
|
const USAGE_SKILL_BASE = `# Connecta usage
|
|
3
3
|
|
|
4
4
|
## The surface
|
|
5
5
|
|
|
6
6
|
Seven tools: \`execute_code\`, \`search_tools\`, \`call_tool\`, \`call_destructive_tool\`, \`authorize_connector\`, \`get_result\`, \`skills\`. Read-only discovery and multi-call work live in a program. Top-level search remains for catalog inspection and approval-required work.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Follow the MCP instructions for routing. Read at most once for syntax or repair.
|
|
9
9
|
|
|
10
10
|
## Inside a program
|
|
11
11
|
|
|
12
|
-
Write one plain-JavaScript async arrow function
|
|
12
|
+
Write one plain-JavaScript async arrow function, without TypeScript or imports. Return reduced JSON-shaped data.
|
|
13
13
|
|
|
14
14
|
The minimum guest API is:
|
|
15
15
|
|
|
16
|
-
- \`<connectorId>.<toolName>(args)\` calls a sanitized shortcut. Non-identifier characters become \`_\`; leading digits gain \`_\`; reserved words gain a trailing \`_\`.
|
|
17
16
|
- \`connecta.call("connector.tool", args)\` uses the canonical address and returns the unwrapped value.
|
|
18
17
|
- \`connecta.search(args)\` returns \`{ tools, total, offset, limit, hasMore }\`; \`connecta.describe(args)\` returns \`{ tools }\`.
|
|
19
|
-
- \`
|
|
20
|
-
- \`console.log(...)\` is captured. \`connecta.emit(block)\`
|
|
18
|
+
- Use \`Promise.all\` for independent calls, or \`Promise.allSettled\` to keep successes and failures.
|
|
19
|
+
- \`console.log(...)\` is captured. \`connecta.emit(block)\` produces rich output.
|
|
21
20
|
|
|
22
21
|
## Discover and select
|
|
23
22
|
|
|
24
|
-
Search
|
|
23
|
+
Search and call in one run when schemas suffice. For unfamiliar result formats, return a small sample and continue; do not guess a parser. Use 2–4 distinctive action/object terms; search distinct operations separately.
|
|
25
24
|
|
|
26
|
-
For top-level catalog inspection or approval-required discovery, omit \`limit\` initially (the default is
|
|
25
|
+
For top-level catalog inspection or approval-required discovery, omit \`limit\` initially (the default is 8), then page with a limit up to 50 if needed. Empty or whitespace-only queries browse all tools. A non-empty query with no ASCII terms returns no matches; mixed input searches with its ASCII terms. \`includeSchemas: "compact"\` adds bounded input and available output shapes. An \`outputSchemaSource: "observed"\` shape is a hint, not a contract. Plain objects expose \`inputKeys\`, \`requiredInputKeys\`, and \`outputKeys\`; truncation flags mark incomplete shapes; matches also carry declared annotations.
|
|
27
26
|
|
|
28
27
|
- \`connecta.search({})\` loads all catalogs. Pass \`connector: "<id>"\` when the integration is obvious. Use \`safety: "readOnly"\` for program calls. These inputs filter discovery; they grant no authority.
|
|
29
|
-
-
|
|
28
|
+
- Use \`includeSchemas: "json"\` for programmatic schema inspection; compact schemas are text, not objects with \`.properties\`. Check connectorTitle for the account/environment, then address, purpose, annotations, inputs, and outputs. Never select only because a result ranks first or has fewer required inputs.
|
|
30
29
|
- Supply every \`requiredInputKey\` from the task or a prior result. For dependencies, match the earlier \`outputKey\` to the later required key. An empty required-key list does not permit invented arguments. Missing \`outputKeys\` means inspect \`outputSchema\`.
|
|
31
30
|
- Use \`connecta.describe({ address })\` or \`{ addresses }\` when a compact schema is truncated or insufficient. Use \`format: "json"\` only for exact constraints. Write the property names the schema displays; never guess positions or aliases.
|
|
32
|
-
- Reduce through available output keys. Treat an observed key as a hint, since later results may omit it or add others. Do not guess collection roots such as \`items\` or \`results\`. If a match
|
|
31
|
+
- Reduce through available output keys. Treat an observed key as a hint, since later results may omit it or add others. Do not guess collection roots such as \`items\` or \`results\`. If a match is missing, re-search or describe. If a result shape is unclear, return a small sample for inspection before continuing.
|
|
33
32
|
- Match provider identifiers and names exactly after resolving them from source data or a connector guide. A broad regular expression that merely finds a plausible value is not identity resolution.
|
|
34
|
-
- Preserve the schema's JSON types exactly: a numeric id is a number, not a numeric-looking string.
|
|
33
|
+
- Preserve the schema's JSON types exactly: a numeric id is a number, not a numeric-looking string.
|
|
35
34
|
- Validate tabular headers, row arrays, and row widths before mapping them. Never let a header or partial row become data.
|
|
36
35
|
|
|
37
36
|
Only tools explicitly annotated \`readOnlyHint: true\` are reachable. The catalog, credential, admission, and read-only gates run below the sandbox; code cannot widen its authority.
|
|
38
37
|
|
|
39
38
|
## Errors and repair
|
|
40
39
|
|
|
41
|
-
Caught Connecta errors expose \`message\`, \`code\`, \`retryable\`, and \`details\`.
|
|
40
|
+
Caught Connecta errors expose \`message\`, \`code\`, \`retryable\`, and \`details\`. Promise rejections retain these fields. Branch on fields, never prose. After a shared argument failure, repair one call before repeating it across other records. Do not retry \`retryable: false\`, and do not retry \`rate_limited\` immediately because portable code has no timer.
|
|
42
41
|
|
|
43
42
|
- \`destructive_tool_requires_approval\`: stop the program and use the returned canonical address with top-level \`call_destructive_tool\`.
|
|
44
43
|
- \`auth_required\`: let the failure reach the model, then use top-level \`authorize_connector\`, give its handoff to the operator, and retry after recovery.
|
|
45
44
|
- A truncated direct-call result: follow its \`get_result\` action. A truncated program result has no page handle; filter, map, or slice inside a new program.
|
|
46
45
|
- Unknown addresses and tools carry scoped search recovery. Use it inside the current run. Do not invent an address.
|
|
47
46
|
|
|
48
|
-
For a direct call, \`resultMode: "value"\` unwraps the result. \`timeoutMs\` sets its deadline.
|
|
47
|
+
For a direct call, \`resultMode: "value"\` unwraps the result. \`timeoutMs\` sets its deadline. Every call makes one attempt; use the returned error classification and retry hint to decide whether to reissue. \`diagnostics: true\` adds timing.
|
|
49
48
|
|
|
50
49
|
\`get_result({ id, offset?, maxBytes? })\` returns \`{ text, offset, nextOffset?, totalBytes }\` for a direct-call result. Both sizes are byte counts: \`maxBytes\` must be a whole number at least 1 and defaults to the deployment cap; \`offset\` must be a whole number at least 0 and defaults to 0. An offset inside a multi-byte character moves back to its first byte, and the response reports the served offset. Follow \`nextOffset\` to reassemble pages. An unknown or expired id is an error.
|
|
51
50
|
|
|
52
|
-
Limits: 20 host calls per run
|
|
51
|
+
Limits: 20 host calls per run and a 15-second deadline per host call.
|
|
53
52
|
|
|
54
53
|
## Runtime portability
|
|
55
54
|
|
|
56
|
-
Portable code uses
|
|
55
|
+
Portable code uses standard JavaScript builtins, \`connecta\`, and \`console.*\`. QuickJS blocks imports and lacks fetch, process, timers, crypto, and WebSocket. Dynamic Workers must use only \`{ loader }\`; bindings, modules, or globalOutbound grant ambient authority. With loader only, environment maps are empty; node:fs/http/https are absent; outbound fetch, WebSocket, node:net, and node:tls are denied; DNS is unresolved. Runtime builtins remain through \`import()\` and \`process.getBuiltinModule()\`, including node:path and cloudflare:workers; this set can drift. Timers, process, crypto, WebSocket, and data: fetch remain. Avoid every runtime-only capability because QuickJS fails.
|
|
57
56
|
|
|
58
57
|
## Examples
|
|
59
58
|
|
|
@@ -61,13 +60,29 @@ One read-only call at a known address:
|
|
|
61
60
|
|
|
62
61
|
\`async () => await connecta.call("crm.get_account", { id: "acct_42" })\`
|
|
63
62
|
|
|
64
|
-
Dependent
|
|
63
|
+
Dependent lookup: verify connector \`ci\`, run 42, and these schema fields first.
|
|
65
64
|
|
|
66
|
-
|
|
65
|
+
\`\`\`js
|
|
66
|
+
async () => {
|
|
67
|
+
const find = async name => {
|
|
68
|
+
const page = await connecta.search({ connector: "ci", query: name, safety: "readOnly", includeSchemas: "compact" });
|
|
69
|
+
if (page.queryAnalysis?.catalogError) throw new Error(JSON.stringify(page.queryAnalysis.catalogError));
|
|
70
|
+
return page.tools.find(tool => tool.name === name);
|
|
71
|
+
};
|
|
72
|
+
const runTool = await find("get_run");
|
|
73
|
+
if (!runTool) return { gap: "Run lookup not resolved" };
|
|
74
|
+
const run = await connecta.call(runTool.address, { runId: 42 });
|
|
75
|
+
if (!run.failedJobId) return { status: run.status, gap: "No failed job identified" };
|
|
76
|
+
const logsTool = await find("get_job_logs");
|
|
77
|
+
if (!logsTool) return { status: run.status, gap: "Job logs not resolved" };
|
|
78
|
+
const logs = await connecta.call(logsTool.address, { jobId: run.failedJobId });
|
|
79
|
+
return logs.filter(row => row.level === "error").map(({ timestamp, message }) => ({ timestamp, message }));
|
|
80
|
+
}
|
|
81
|
+
\`\`\`
|
|
67
82
|
|
|
68
|
-
##
|
|
83
|
+
## Media output
|
|
69
84
|
|
|
70
|
-
\`connecta.emit\` accepts text, image, or audio blocks and delivers them only on success.
|
|
85
|
+
\`connecta.emit\` accepts text, image, or audio blocks and delivers them only on success. Its byte and block budgets are separate from the JSON return budget. Return data for the client to render as a view.
|
|
71
86
|
|
|
72
87
|
`;
|
|
73
88
|
/** Deployment-scoped guide routing appended to the shared usage guide. */
|
|
@@ -95,12 +110,33 @@ export function hasConnectorGuides(connectors) {
|
|
|
95
110
|
function usageSkill(_connectors) {
|
|
96
111
|
return USAGE_SKILL;
|
|
97
112
|
}
|
|
113
|
+
const INVESTIGATE_SKILL = `# Investigate across services
|
|
114
|
+
|
|
115
|
+
## Plan the investigation
|
|
116
|
+
|
|
117
|
+
Start from the user's question and the evidence that would answer it. Select the app, account, and environment from connector titles, purposes, and relevant guides before looking up records. Reuse verified ids within the task; never carry an id across connectors just because its name matches.
|
|
118
|
+
|
|
119
|
+
- Purchase verification: resolve the same customer and environment across payment, subscription access, and analytics. Check each separately; a recorded payment does not prove access, and a missing analytics event does not prove payment failure.
|
|
120
|
+
- Experiment checks: confirm the project, experiment, time window, and exposure population before comparing outcomes. Return the requested comparison and any missing evidence; do not expand into an unrelated analytics audit.
|
|
121
|
+
- Customer or deployment investigations: locate the exact customer or deployment first, then follow only the records needed to explain the reported symptom. Use provider links or ids so the answer can be checked.
|
|
122
|
+
|
|
123
|
+
Establish capability limits early. A partial search is not proof that a tool is absent, and an unavailable catalog is not an empty dataset. Try a scoped search for the missing operation, inspect its guide when relevant, and distinguish unsupported work from missing data or authorization. If the required evidence is unavailable, return what was verified and the specific gap instead of approximating a different question through repeated calls.
|
|
124
|
+
|
|
125
|
+
A host transport error that requires reconnecting this MCP server cannot be repaired by a downstream tool. Reconnect in the host; do not repeatedly call \`authorize_connector\` through the failed connection.
|
|
126
|
+
|
|
127
|
+
Return the answer first, then the evidence and any unresolved gap. Include the app/environment, time window, and source ids or links needed to check it. Separate observed facts from inferences. Stop when the requested evidence is sufficient.
|
|
128
|
+
`;
|
|
98
129
|
const AVAILABLE_SKILLS = [
|
|
99
130
|
{
|
|
100
131
|
name: "usage",
|
|
101
132
|
description: "How to route work between one execute_code program and Connecta's explicit call, authorization, and result tools.",
|
|
102
133
|
content: usageSkill,
|
|
103
134
|
},
|
|
135
|
+
{
|
|
136
|
+
name: "investigate",
|
|
137
|
+
description: "Plan purchase verification, experiment checks, and customer or deployment investigations across services; resolve scope and capability limits before querying.",
|
|
138
|
+
content: () => INVESTIGATE_SKILL,
|
|
139
|
+
},
|
|
104
140
|
];
|
|
105
141
|
/**
|
|
106
142
|
* Namespace for operator-authored per-connector guides. Built-in skill names
|
package/dist/types.d.ts
CHANGED
|
@@ -132,6 +132,8 @@ export interface CredentialTestResult {
|
|
|
132
132
|
message?: string;
|
|
133
133
|
}
|
|
134
134
|
export interface ConnectorContext {
|
|
135
|
+
/** Explicit downstream consent initiation, never set by status/catalog/calls. */
|
|
136
|
+
allowAuthorization?: boolean;
|
|
135
137
|
/** Storage namespaced to this connector. */
|
|
136
138
|
storage: KVStorage;
|
|
137
139
|
logger: Logger;
|
|
@@ -312,19 +314,6 @@ export interface Connector {
|
|
|
312
314
|
* server's RFC 9207 `iss` response parameter for SDK validation.
|
|
313
315
|
*/
|
|
314
316
|
finishAuth?(code: string, ctx: ConnectorContext, callbackParams?: URLSearchParams): Promise<void>;
|
|
315
|
-
/**
|
|
316
|
-
* Optional: serve a connector-owned HTTP route — for example a signed
|
|
317
|
-
* download link minted by one of the connector's tools. Called only after
|
|
318
|
-
* every built-in route misses, so a connector can never shadow `/mcp`,
|
|
319
|
-
* `/`, `/credentials`, `/activity`, `/health`, or the credential API. The
|
|
320
|
-
* first connector to return a Response wins, in registration order; return
|
|
321
|
-
* null to decline. See documentation/architecture.md route 9.
|
|
322
|
-
*
|
|
323
|
-
* These routes are PUBLIC: connecta applies no auth gate to them. A
|
|
324
|
-
* connector that serves data here MUST authenticate the request itself — for
|
|
325
|
-
* example with a signed capability token in the URL.
|
|
326
|
-
*/
|
|
327
|
-
handleRequest?(request: Request, ctx: ConnectorContext): Promise<Response | null>;
|
|
328
317
|
}
|
|
329
318
|
export interface ConnectorUsageGuide {
|
|
330
319
|
/** Markdown returned verbatim by `skills({ name: "connector:<id>" })`. */
|
|
@@ -355,8 +344,7 @@ export interface ExecutorProvider {
|
|
|
355
344
|
fns: Record<string, (...args: unknown[]) => Promise<unknown>>;
|
|
356
345
|
/**
|
|
357
346
|
* Optional trusted sandbox-side setup run after provider globals exist.
|
|
358
|
-
* Connecta uses this to
|
|
359
|
-
* materializing one host closure per tool. This is host-authored code, never
|
|
347
|
+
* Connecta uses this to restore typed host errors. This is host-authored code, never
|
|
360
348
|
* model input. See documentation/code-mode.md#what-an-executor-must-implement.
|
|
361
349
|
*/
|
|
362
350
|
prelude?: string;
|