@zackbart/connecta 0.20.0 → 0.21.1
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 +93 -0
- package/README.md +3 -1
- package/bin/connecta.mjs +23 -6
- package/dist/auth/bearer.d.ts +2 -2
- package/dist/auth/bearer.js +2 -2
- package/dist/auth/clerk.js +1 -0
- package/dist/auth/cloudflare-access.d.ts +8 -0
- package/dist/auth/cloudflare-access.js +66 -0
- package/dist/execute.js +12 -6
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/meta-tools.js +3 -3
- package/dist/operator-ui/generated.js +1 -1
- package/dist/operator-ui/model.d.ts +3 -3
- package/dist/operator-ui/view.d.ts +1 -1
- package/dist/operator-ui/view.js +6 -3
- package/dist/providers/cloudflare.js +233 -61
- package/dist/providers/linear.js +7 -0
- package/dist/providers/mixpanel.js +6 -4
- package/dist/providers/revenuecat.js +2 -1
- package/dist/providers/stripe.js +7 -6
- package/dist/routes/access-tokens.d.ts +1 -1
- package/dist/routes/access-tokens.js +2 -2
- package/dist/routes/activity.js +2 -2
- package/dist/routes/credentials.js +1 -1
- package/dist/routes/mcp.js +1 -1
- package/dist/routes/oauth.js +1 -1
- package/dist/routes/shared.d.ts +4 -4
- package/dist/routes/shared.js +10 -10
- package/dist/routes/ui.js +12 -9
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +7 -4
- package/dist/types.d.ts +37 -22
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +3 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +7 -4
- package/documentation/auth.md +71 -7
- package/documentation/cloudflare.md +19 -1
- package/documentation/code-mode.md +4 -4
- package/documentation/linear.md +6 -4
- package/documentation/meta-tools.md +22 -20
- package/documentation/mixpanel.md +5 -3
- package/documentation/operations.md +28 -13
- package/documentation/operator-ui.md +21 -5
- package/documentation/provider-conventions.md +4 -2
- package/documentation/revenuecat.md +21 -21
- package/documentation/stripe.md +14 -13
- package/documentation/upgrading.md +77 -4
- package/ethos.md +2 -3
- package/examples/worker/README.md +52 -32
- package/examples/worker/src/index.ts +32 -38
- package/examples/worker/wrangler.jsonc +12 -4
- package/package.json +5 -1
- package/templates/node/package.json +1 -1
package/dist/providers/stripe.js
CHANGED
|
@@ -37,16 +37,16 @@ const READ_ONLY_TOOLS = new Set([
|
|
|
37
37
|
"stripe_api_search",
|
|
38
38
|
"stripe_api_details",
|
|
39
39
|
"stripe_api_read",
|
|
40
|
-
"get_stripe_account_info",
|
|
41
40
|
"get_balance_summary",
|
|
41
|
+
"list_available_accounts_or_orgs",
|
|
42
|
+
"manage_stripe_accounts",
|
|
42
43
|
"search_stripe_documentation",
|
|
43
|
-
"stripe_implementation_planner",
|
|
44
44
|
]);
|
|
45
|
-
/** Reviewed writes, including
|
|
45
|
+
/** Reviewed writes, including mixed read/create tools: `documentation/stripe.md`. */
|
|
46
46
|
const WRITE_TOOLS = new Map([
|
|
47
47
|
["stripe_api_write", "destructive"],
|
|
48
|
-
["
|
|
49
|
-
["
|
|
48
|
+
["stripe_implementation_planner", "additive"],
|
|
49
|
+
["stripe_analytics", "additive"],
|
|
50
50
|
["send_stripe_mcp_feedback", "additive"],
|
|
51
51
|
]);
|
|
52
52
|
/** Release-reviewed manifest; see provider conventions P5 and P13. */
|
|
@@ -149,7 +149,8 @@ ${accountInstructions
|
|
|
149
149
|
function sharedUsageGuide(rate) {
|
|
150
150
|
return `
|
|
151
151
|
- Four generic tools reach any Stripe API method. Find the method with \`stripe_api_search\`, read its parameters with \`stripe_api_details\`, then call \`stripe_api_read\` (GET) or \`stripe_api_write\` (POST/PATCH/PUT/DELETE). Never guess a path or a parameter name — \`stripe_api_details\` is cheaper than a rejected write.
|
|
152
|
-
- Prefer a dedicated tool when one covers the task: \`
|
|
152
|
+
- Prefer a dedicated tool when one covers the task: \`get_balance_summary\` for balances and \`stripe_analytics\` for Sigma or Metrics reporting. Use \`stripe_api_search\` for everything else instead of assuming a retired dedicated tool still exists.
|
|
153
|
+
- \`stripe_implementation_planner\` and the query-execution intents of \`stripe_analytics\` create provider-side planning or query-run state. Connecta therefore routes both through \`call_destructive_tool\` as non-destructive writes; their retrieval paths stay behind the same tool boundary.
|
|
153
154
|
- \`stripe_api_write\` carries the blast radius of the entire write API — every POST, PATCH, PUT, and DELETE, from a customer edit to a subscription cancellation. State the method and path explicitly; expect approval on every call.
|
|
154
155
|
- Lists are cursor-paginated: \`limit\` defaults to 10 and caps at 100, \`starting_after\` and \`ending_before\` take an object id and are mutually exclusive, and \`has_more\` says whether to continue. Page inside \`execute_code\`.
|
|
155
156
|
- Any \`stripe_api_read\` list or \`stripe_api_search\` that returns full objects belongs inside \`execute_code\`, projected to the fields the question needs before \`return\`. Neither \`limit\` nor \`expand\` substitutes for that: an unprojected list of customers or invoices truncates long before it answers, and a projected one keeps the customer's name, email, and address out of the transcript.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type RouteContext } from "./shared.js";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Interactive-operator lifecycle for deployment access tokens. The token itself
|
|
4
4
|
* is deliberately never an administrator credential and cannot reach here.
|
|
5
5
|
*/
|
|
6
6
|
export declare function routeAccessTokens(context: RouteContext): Promise<Response | null>;
|
|
@@ -28,7 +28,7 @@ async function readName(request) {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
31
|
+
* Interactive-operator lifecycle for deployment access tokens. The token itself
|
|
32
32
|
* is deliberately never an administrator credential and cannot reach here.
|
|
33
33
|
*/
|
|
34
34
|
export async function routeAccessTokens(context) {
|
|
@@ -46,7 +46,7 @@ export async function routeAccessTokens(context) {
|
|
|
46
46
|
if (mutating && !isSameOrigin(request, baseUrl)) {
|
|
47
47
|
return privateJson({ error: "same-origin request required" }, { status: 403 });
|
|
48
48
|
}
|
|
49
|
-
const admin = await authorizeUiAdmin(request, baseUrl, opts.auth, "access token management");
|
|
49
|
+
const admin = await authorizeUiAdmin(request, baseUrl, opts.auth, "access token management", context.runtimeContext);
|
|
50
50
|
if (!admin.ok)
|
|
51
51
|
return admin.response;
|
|
52
52
|
const id = match[1];
|
package/dist/routes/activity.js
CHANGED
|
@@ -128,13 +128,13 @@ async function enrichActivityActorLabels(page, auth) {
|
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
130
|
export async function routeActivity(context) {
|
|
131
|
-
const { path, request, url, baseUrl, opts } = context;
|
|
131
|
+
const { path, request, url, baseUrl, opts, runtimeContext } = context;
|
|
132
132
|
if (path !== "/ui/activity")
|
|
133
133
|
return null;
|
|
134
134
|
if (request.method !== "GET") {
|
|
135
135
|
return privateJson({ error: "method not allowed" }, { status: 405 });
|
|
136
136
|
}
|
|
137
|
-
const authz = await authorize(request, baseUrl, opts.auth);
|
|
137
|
+
const authz = await authorize(request, baseUrl, opts.auth, runtimeContext);
|
|
138
138
|
if (!authz.ok)
|
|
139
139
|
return authz.response;
|
|
140
140
|
if (opts.activityReadGate &&
|
|
@@ -73,7 +73,7 @@ async function handleCredentialRequest(context, connectorId, action) {
|
|
|
73
73
|
if (!isSameOrigin(request, baseUrl)) {
|
|
74
74
|
return privateJson({ error: "same-origin request required" }, { status: 403 });
|
|
75
75
|
}
|
|
76
|
-
const admin = await authorizeUiAdmin(request, baseUrl, opts.auth);
|
|
76
|
+
const admin = await authorizeUiAdmin(request, baseUrl, opts.auth, "credential management", context.runtimeContext);
|
|
77
77
|
if (!admin.ok)
|
|
78
78
|
return admin.response;
|
|
79
79
|
const connector = opts.registry.getConnector(connectorId);
|
package/dist/routes/mcp.js
CHANGED
|
@@ -329,7 +329,7 @@ export function createMcpRoute(opts) {
|
|
|
329
329
|
throw error;
|
|
330
330
|
}
|
|
331
331
|
try {
|
|
332
|
-
const authz = await authorize(request, baseUrl, opts.auth);
|
|
332
|
+
const authz = await authorize(request, baseUrl, opts.auth, runtimeContext);
|
|
333
333
|
if (!authz.ok) {
|
|
334
334
|
return releaseAdmissionWithResponse(withMcpCors(authz.response), admission, request.signal);
|
|
335
335
|
}
|
package/dist/routes/oauth.js
CHANGED
|
@@ -7,7 +7,7 @@ async function handleOAuthManagementRequest(context, connectorId) {
|
|
|
7
7
|
if (!isSameOrigin(request, baseUrl)) {
|
|
8
8
|
return privateJson({ error: "same-origin request required" }, { status: 403 });
|
|
9
9
|
}
|
|
10
|
-
const admin = await authorizeUiAdmin(request, baseUrl, opts.auth, "OAuth management");
|
|
10
|
+
const admin = await authorizeUiAdmin(request, baseUrl, opts.auth, "OAuth management", context.runtimeContext);
|
|
11
11
|
if (!admin.ok)
|
|
12
12
|
return admin.response;
|
|
13
13
|
const connector = opts.registry.getConnector(connectorId);
|
package/dist/routes/shared.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { CredentialVault } from "../credentials.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 { ConnectaBranding, Executor, InboundAuth, Logger } from "../types.js";
|
|
8
|
+
import type { ConnectaBranding, Executor, InboundAuth, InboundAuthRuntimeContext, Logger } from "../types.js";
|
|
9
9
|
export { msg } from "../errors.js";
|
|
10
10
|
export interface ServerOptions {
|
|
11
11
|
registry: Registry;
|
|
@@ -40,7 +40,7 @@ export interface ServerOptions {
|
|
|
40
40
|
/** Optional browser UI and OAuth result-page labels. */
|
|
41
41
|
branding?: ConnectaBranding | undefined;
|
|
42
42
|
}
|
|
43
|
-
export interface RuntimeExecutionContext {
|
|
43
|
+
export interface RuntimeExecutionContext extends InboundAuthRuntimeContext {
|
|
44
44
|
waitUntil(promise: Promise<unknown>): void;
|
|
45
45
|
}
|
|
46
46
|
export interface RouteContext {
|
|
@@ -64,7 +64,7 @@ export declare function privateJson(body: unknown, init?: ResponseInit): Respons
|
|
|
64
64
|
*/
|
|
65
65
|
export declare function loggableValue(requested: string): string;
|
|
66
66
|
export declare function activityActorNamespace(provider: InboundAuth): string | undefined;
|
|
67
|
-
export declare function authorize(request: Request, baseUrl: string, auth: InboundAuth[]): Promise<{
|
|
67
|
+
export declare function authorize(request: Request, baseUrl: string, auth: InboundAuth[], runtimeContext?: RuntimeExecutionContext): Promise<{
|
|
68
68
|
ok: true;
|
|
69
69
|
actor: ActivityActor;
|
|
70
70
|
/** True only when the admitting provider can also authorize UI mutation. */
|
|
@@ -73,7 +73,7 @@ export declare function authorize(request: Request, baseUrl: string, auth: Inbou
|
|
|
73
73
|
ok: false;
|
|
74
74
|
response: Response;
|
|
75
75
|
}>;
|
|
76
|
-
export declare function authorizeUiAdmin(request: Request, baseUrl: string, auth: InboundAuth[], purpose?: string): Promise<{
|
|
76
|
+
export declare function authorizeUiAdmin(request: Request, baseUrl: string, auth: InboundAuth[], purpose?: string, runtimeContext?: RuntimeExecutionContext): Promise<{
|
|
77
77
|
ok: true;
|
|
78
78
|
userId: string;
|
|
79
79
|
} | {
|
package/dist/routes/shared.js
CHANGED
|
@@ -29,13 +29,13 @@ export function activityActorNamespace(provider) {
|
|
|
29
29
|
? provider.activityActorNamespace
|
|
30
30
|
: undefined;
|
|
31
31
|
}
|
|
32
|
-
export async function authorize(request, baseUrl, auth) {
|
|
32
|
+
export async function authorize(request, baseUrl, auth, runtimeContext) {
|
|
33
33
|
if (auth.length === 0) {
|
|
34
34
|
return { ok: true, actor: { kind: "anonymous" } };
|
|
35
35
|
}
|
|
36
36
|
let lastResponse = null;
|
|
37
37
|
for (const provider of auth) {
|
|
38
|
-
const result = await provider.authorize(request, baseUrl);
|
|
38
|
+
const result = await provider.authorize(request, baseUrl, runtimeContext);
|
|
39
39
|
if (result.ok) {
|
|
40
40
|
const subjectId = result.subjectId ?? result.userId;
|
|
41
41
|
const actorNamespace = activityActorNamespace(provider);
|
|
@@ -48,7 +48,7 @@ export async function authorize(request, baseUrl, auth) {
|
|
|
48
48
|
? { namespace: actorNamespace }
|
|
49
49
|
: {}),
|
|
50
50
|
},
|
|
51
|
-
...(result.userId && provider.
|
|
51
|
+
...(result.userId && provider.interactiveOperator
|
|
52
52
|
? { uiAdminEligible: true }
|
|
53
53
|
: {}),
|
|
54
54
|
};
|
|
@@ -67,25 +67,25 @@ export async function authorize(request, baseUrl, auth) {
|
|
|
67
67
|
}),
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
|
-
export async function authorizeUiAdmin(request, baseUrl, auth, purpose = "credential management") {
|
|
70
|
+
export async function authorizeUiAdmin(request, baseUrl, auth, purpose = "credential management", runtimeContext) {
|
|
71
71
|
// Operator mutation is intentionally narrower than /mcp and /ui/data: only
|
|
72
|
-
// an interactive
|
|
72
|
+
// an interactive provider may admit it. A static bearer token is useful
|
|
73
73
|
// for headless tool calls but must not become a deployment-admin key.
|
|
74
74
|
//
|
|
75
|
-
// Every
|
|
76
|
-
// the first would make admission depend on config order: a failed gate or
|
|
75
|
+
// Every interactive provider gets a turn, the way the /mcp gate does.
|
|
76
|
+
// Stopping at the first would make admission depend on config order: a failed gate or
|
|
77
77
|
// missing user may simply mean a later provider is the one meant to admit.
|
|
78
78
|
// The last refusal is returned if none do.
|
|
79
|
-
const providers = auth.filter((candidate) => candidate.
|
|
79
|
+
const providers = auth.filter((candidate) => candidate.interactiveOperator);
|
|
80
80
|
if (providers.length === 0) {
|
|
81
81
|
return {
|
|
82
82
|
ok: false,
|
|
83
|
-
response: privateJson({ error: `${purpose} requires
|
|
83
|
+
response: privateJson({ error: `${purpose} requires interactive operator authentication` }, { status: 403 }),
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
86
|
let lastResponse = null;
|
|
87
87
|
for (const provider of providers) {
|
|
88
|
-
const result = await provider.authorize(request, baseUrl);
|
|
88
|
+
const result = await provider.authorize(request, baseUrl, runtimeContext);
|
|
89
89
|
if (!result.ok) {
|
|
90
90
|
lastResponse = result.response;
|
|
91
91
|
continue;
|
package/dist/routes/ui.js
CHANGED
|
@@ -37,7 +37,7 @@ function uiScriptNonce() {
|
|
|
37
37
|
return btoa(binary);
|
|
38
38
|
}
|
|
39
39
|
export async function routeUi(context) {
|
|
40
|
-
const { request, url, path, baseUrl, opts, defer } = context;
|
|
40
|
+
const { request, url, path, baseUrl, opts, defer, runtimeContext } = context;
|
|
41
41
|
if (request.method === "GET" && path === "/favicon.svg") {
|
|
42
42
|
return new Response(opts.branding?.favicon?.svg ?? CONNECTA_FAVICON_SVG, {
|
|
43
43
|
headers: {
|
|
@@ -73,7 +73,10 @@ export async function routeUi(context) {
|
|
|
73
73
|
}
|
|
74
74
|
// Open shell — carries no operator data; everything comes from the
|
|
75
75
|
// authenticated /ui/* APIs after the browser establishes a session.
|
|
76
|
-
const
|
|
76
|
+
const ambient = runtimeContext?.access
|
|
77
|
+
? opts.auth.find((provider) => provider.uiAuth?.kind === "cloudflare-access")?.uiAuth
|
|
78
|
+
: undefined;
|
|
79
|
+
const uiAuth = ambient ?? opts.auth.find((provider) => provider.uiAuth && provider.uiAuth.kind !== "cloudflare-access")?.uiAuth;
|
|
77
80
|
const mcpUrl = new URL("/mcp", baseUrl).toString();
|
|
78
81
|
// Nonce the page's inline script (and the Clerk loader). 'strict-dynamic'
|
|
79
82
|
// lets scripts the nonced Clerk loader injects at runtime execute; the
|
|
@@ -96,28 +99,28 @@ export async function routeUi(context) {
|
|
|
96
99
|
}
|
|
97
100
|
if (path !== "/ui/data")
|
|
98
101
|
return null;
|
|
99
|
-
const authz = await authorize(request, baseUrl, opts.auth);
|
|
102
|
+
const authz = await authorize(request, baseUrl, opts.auth, runtimeContext);
|
|
100
103
|
if (!authz.ok)
|
|
101
104
|
return authz.response;
|
|
102
|
-
const
|
|
105
|
+
const eligibleOperator = authz.uiAdminEligible === true;
|
|
103
106
|
const credentialManagement = credentialManagementCapability({
|
|
104
|
-
|
|
107
|
+
eligibleOperator,
|
|
105
108
|
hasCredentialSlots: opts.registry
|
|
106
109
|
.listConnectors()
|
|
107
110
|
.some((connector) => Boolean(connector.credential)),
|
|
108
111
|
hasCredentialVault: Boolean(opts.credentialVault),
|
|
109
112
|
});
|
|
110
113
|
// As with connector credentials, a Bearer-authenticated observer learns
|
|
111
|
-
// only that
|
|
114
|
+
// only that an interactive operator is required, not whether this deployment has opted into
|
|
112
115
|
// token issuance. Configuration topology is operator data.
|
|
113
|
-
const accessTokenManagement = !
|
|
114
|
-
? "
|
|
116
|
+
const accessTokenManagement = !eligibleOperator
|
|
117
|
+
? "requires_operator"
|
|
115
118
|
: opts.accessTokens
|
|
116
119
|
? "available"
|
|
117
120
|
: "not_configured";
|
|
118
121
|
const data = await buildUiData(opts.registry, baseUrl, opts.serverInfo,
|
|
119
122
|
// The static headless bearer may read connector health, but only a
|
|
120
123
|
// Clerk-authenticated operator receives credential metadata.
|
|
121
|
-
|
|
124
|
+
eligibleOperator ? opts.credentialVault : undefined, Boolean(opts.activity?.list), credentialManagement, defer, eligibleOperator, opts.discoveryConcurrency, accessTokenManagement);
|
|
122
125
|
return privateJson(data);
|
|
123
126
|
}
|
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.
|
|
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 one execute_code program that discovers, calls, and returns the answer; use the same route for reduction, multiple or dependent calls, loops, joins, or branches. 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. connecta.ui(html) exists only inside execute_code, not in connector search; return the same summary data the HTML renders. 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,11 +1,11 @@
|
|
|
1
|
-
export const CONNECTA_INSTRUCTIONS = 'Choose a route before discovery.
|
|
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 one execute_code program that discovers, calls, and returns the answer; use the same route for reduction, multiple or dependent calls, loops, joins, or branches. 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. connecta.ui(html) exists only inside execute_code, not in connector search; return the same summary data the HTML renders. 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
|
-
Seven tools: \`execute_code\`, \`search_tools\`, \`call_tool\`, \`call_destructive_tool\`, \`authorize_connector\`, \`get_result\`, \`skills\`.
|
|
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
|
-
The always-loaded MCP instructions are authoritative for choosing the top-level route. Read this skill at most once
|
|
8
|
+
The always-loaded MCP instructions are authoritative for choosing the top-level route. Read this skill at most once, and only when their program workflow is insufficient or a run needs repair.
|
|
9
9
|
|
|
10
10
|
## Inside a program
|
|
11
11
|
|
|
@@ -23,13 +23,16 @@ The minimum guest API is:
|
|
|
23
23
|
|
|
24
24
|
Search inside the run and finish the task there. A discovery-only program wastes a round trip. Use 2–4 distinctive action/object terms, not the full request. Use separate short searches for distinct operations.
|
|
25
25
|
|
|
26
|
-
For top-level
|
|
26
|
+
For top-level catalog inspection or approval-required discovery, omit \`limit\` initially (the default is 10), 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 observed shape carries \`outputSchemaSource: "observed"\`; treat it as routing evidence rather than a provider contract. Plain objects expose \`inputKeys\`, \`requiredInputKeys\`, and \`outputKeys\`; truncation flags mark incomplete shapes; matches also carry declared annotations.
|
|
27
27
|
|
|
28
28
|
- \`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
29
|
- Request \`includeSchemas: "compact"\`. Check address, purpose, annotations, required inputs, truncation, safety, and available outputs. Never select only because a result ranks first or has fewer required inputs.
|
|
30
30
|
- 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
31
|
- 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
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 or result key is missing, inspect, re-search, or describe inside the same run instead of returning discovery for another call.
|
|
33
|
+
- 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. Call the search and describe functions directly; batch accepts canonical connector tool addresses, not guest API function names.
|
|
35
|
+
- Validate tabular headers, row arrays, and row widths before mapping them. Never let a header or partial row become data.
|
|
33
36
|
|
|
34
37
|
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.
|
|
35
38
|
|
package/dist/types.d.ts
CHANGED
|
@@ -426,35 +426,48 @@ export type AuthResult = {
|
|
|
426
426
|
};
|
|
427
427
|
/** Public browser-auth configuration exposed to connecta's status UI. */
|
|
428
428
|
export type UiAuthConfig = {
|
|
429
|
+
kind: "cloudflare-access";
|
|
430
|
+
} | {
|
|
429
431
|
kind: "clerk";
|
|
430
432
|
publishableKey: string;
|
|
431
433
|
/**
|
|
432
|
-
* Origin the operator shell fetches its browser sign-in loader from.
|
|
433
|
-
* `https:` URL** — the value lands in a `<script
|
|
434
|
-
* stricter than the branding href gate: no `http:`,
|
|
435
|
-
* no root-relative form (a relative path is
|
|
436
|
-
* shipped `clerkAuth` adapter derives this
|
|
437
|
-
* Clerk's Frontend API is always https, so
|
|
438
|
-
* carve-out. A value that fails the gate
|
|
439
|
-
* the page's inline auth config:
|
|
440
|
-
*
|
|
441
|
-
* warning.
|
|
434
|
+
* Origin the operator shell fetches its browser sign-in loader from.
|
|
435
|
+
* **Must be an absolute `https:` URL** — the value lands in a `<script
|
|
436
|
+
* src>`, so the gate is stricter than the branding href gate: no `http:`,
|
|
437
|
+
* no loopback exemption, and no root-relative form (a relative path is
|
|
438
|
+
* rejected, not resolved). The shipped `clerkAuth` adapter derives this
|
|
439
|
+
* from the publishable key and Clerk's Frontend API is always https, so
|
|
440
|
+
* nothing legitimate needs a carve-out. A value that fails the gate
|
|
441
|
+
* reaches neither the loader tag nor the page's inline auth config:
|
|
442
|
+
* operator pages render without it and report that Clerk could not load,
|
|
443
|
+
* and `createConnecta` names the drop in a startup warning.
|
|
442
444
|
*/
|
|
443
445
|
frontendApiUrl: string;
|
|
444
446
|
/**
|
|
445
|
-
* Hosted Account Portal sign-in address, handed to `Clerk.load`. **Must
|
|
446
|
-
* absolute `https:` URL** — the same gate `frontendApiUrl` passes,
|
|
447
|
-
* this value is where Clerk *navigates* the operator's browser.
|
|
448
|
-
* Portal address is always https, so the stricter gate costs
|
|
449
|
-
* value that fails it (a `javascript:`/`data:` payload,
|
|
450
|
-
* address, a relative path) reaches no part of the
|
|
451
|
-
* through Clerk's default instead, and
|
|
452
|
-
* startup warning.
|
|
447
|
+
* Hosted Account Portal sign-in address, handed to `Clerk.load`. **Must
|
|
448
|
+
* be an absolute `https:` URL** — the same gate `frontendApiUrl` passes,
|
|
449
|
+
* because this value is where Clerk *navigates* the operator's browser.
|
|
450
|
+
* An Account Portal address is always https, so the stricter gate costs
|
|
451
|
+
* nothing real: a value that fails it (a `javascript:`/`data:` payload,
|
|
452
|
+
* a cleartext `http:` address, a relative path) reaches no part of the
|
|
453
|
+
* page, the shell signs in through Clerk's default instead, and
|
|
454
|
+
* `createConnecta` names the drop in a startup warning.
|
|
453
455
|
*/
|
|
454
456
|
signInUrl?: string;
|
|
455
457
|
/** Hosted Account Portal sign-up address. Gated exactly like `signInUrl`. */
|
|
456
458
|
signUpUrl?: string;
|
|
457
459
|
};
|
|
460
|
+
/**
|
|
461
|
+
* Runtime identity context an inbound-auth provider may consume. The shape is
|
|
462
|
+
* deliberately structural: core stays Web-API-only while a Worker can pass
|
|
463
|
+
* Cloudflare's authenticated `ctx.access` object through unchanged.
|
|
464
|
+
*/
|
|
465
|
+
export interface InboundAuthRuntimeContext {
|
|
466
|
+
readonly access?: {
|
|
467
|
+
readonly aud: string;
|
|
468
|
+
getIdentity(): Promise<Record<string, unknown> | undefined>;
|
|
469
|
+
};
|
|
470
|
+
}
|
|
458
471
|
/**
|
|
459
472
|
* Optional labels and marks used by the browser UI and OAuth result pages.
|
|
460
473
|
* Every deployment-identifying string and image is configurable here — nothing
|
|
@@ -493,9 +506,11 @@ export interface ConnectaBranding {
|
|
|
493
506
|
/** `theme-color` meta value. Defaults to "#ffffff". */
|
|
494
507
|
themeColor?: string;
|
|
495
508
|
}
|
|
496
|
-
/** An inbound authentication provider (bearer token,
|
|
509
|
+
/** An inbound authentication provider (bearer token, interactive identity, ...). */
|
|
497
510
|
export interface InboundAuth {
|
|
498
511
|
kind: string;
|
|
512
|
+
/** This provider may admit a human identity to operator mutation routes. */
|
|
513
|
+
interactiveOperator?: true;
|
|
499
514
|
/**
|
|
500
515
|
* Stable, non-secret namespace of the identity directory behind
|
|
501
516
|
* `activityActorLabel`. Stored with new activity actors so two providers with
|
|
@@ -512,13 +527,13 @@ export interface InboundAuth {
|
|
|
512
527
|
*/
|
|
513
528
|
activityActorLabel?(subjectId: string): string | undefined | Promise<string | undefined>;
|
|
514
529
|
/**
|
|
515
|
-
* Optional browser sign-in configuration. When present, operator pages use
|
|
516
|
-
* provider instead of asking the operator to paste a static bearer secret.
|
|
530
|
+
* Optional browser sign-in configuration. When present, operator pages use
|
|
531
|
+
* the provider instead of asking the operator to paste a static bearer secret.
|
|
517
532
|
*/
|
|
518
533
|
uiAuth?: UiAuthConfig;
|
|
519
534
|
/** Serve/short-circuit .well-known + OPTIONS. Return null when not handled. */
|
|
520
535
|
handleMetadata?(request: Request, baseUrl: string): Response | null | Promise<Response | null>;
|
|
521
536
|
/** Attempt to authorize a request. */
|
|
522
|
-
authorize(request: Request, baseUrl: string): AuthResult | Promise<AuthResult>;
|
|
537
|
+
authorize(request: Request, baseUrl: string, runtimeContext?: InboundAuthRuntimeContext): AuthResult | Promise<AuthResult>;
|
|
523
538
|
}
|
|
524
539
|
export {};
|
package/dist/ui.d.ts
CHANGED
|
@@ -73,7 +73,7 @@ export type OperatorPage = "connections" | "credentials" | "tokens" | "activity"
|
|
|
73
73
|
export declare function operatorPageForPath(path: string): OperatorPage | undefined;
|
|
74
74
|
export declare function operatorPageTitle(page: OperatorPage, configuredTitle: string): string;
|
|
75
75
|
export declare function credentialManagementCapability(input: {
|
|
76
|
-
|
|
76
|
+
eligibleOperator: boolean;
|
|
77
77
|
hasCredentialSlots: boolean;
|
|
78
78
|
hasCredentialVault: boolean;
|
|
79
79
|
}): CredentialManagementCapability;
|
package/dist/ui.js
CHANGED
|
@@ -208,8 +208,8 @@ export function operatorPageTitle(page, configuredTitle) {
|
|
|
208
208
|
return `${OPERATOR_PAGE_LABELS[page]} — ${configuredTitle}`;
|
|
209
209
|
}
|
|
210
210
|
export function credentialManagementCapability(input) {
|
|
211
|
-
if (!input.
|
|
212
|
-
return "
|
|
211
|
+
if (!input.eligibleOperator)
|
|
212
|
+
return "requires_operator";
|
|
213
213
|
if (!input.hasCredentialSlots)
|
|
214
214
|
return "no_slots";
|
|
215
215
|
if (!input.hasCredentialVault)
|
|
@@ -223,7 +223,7 @@ export function credentialManagementCapability(input) {
|
|
|
223
223
|
*/
|
|
224
224
|
export async function buildUiData(registry, baseUrl, serverInfo, credentialVault, activityEnabled = false, credentialManagement = credentialVault
|
|
225
225
|
? "available"
|
|
226
|
-
: "
|
|
226
|
+
: "requires_operator", defer, oauthManagement = false, discoveryConcurrency, accessTokenManagement = "not_configured") {
|
|
227
227
|
const requestScope = {};
|
|
228
228
|
const connectorSet = registry.listConnectors();
|
|
229
229
|
const concurrency = resolveDiscoveryConcurrency(discoveryConcurrency);
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
|
@@ -48,6 +48,7 @@ read top to bottom.
|
|
|
48
48
|
| Order | Route | Notes |
|
|
49
49
|
| --- | --- | --- |
|
|
50
50
|
| 0 | HTTPS upgrade | 308 to `publicUrl` when it is HTTPS and the request arrived over HTTP. Path and query are *assigned* onto the configured URL, never resolved against it, so a `//host` pathname cannot replace the deployment origin. `/health` is exempt: a loopback container probe must not depend on public DNS and TLS. `/ui` is canonicalized to `/` while upgrading. |
|
|
51
|
+
| 0 | Cloudflare Access (Worker deployment, when enabled) | Edge admission before this route table. Managed OAuth owns its challenge and discovery metadata; an admitted direct invocation carries trusted identity in `ctx.access`. |
|
|
51
52
|
| 1 | `/ui/access-tokens[/<id>]`, `/ui/credentials/<id>[/<action>]`, `/ui/oauth/<id>` | Private mutation routes, matched **first** so nothing can shadow them and so they own their own `OPTIONS` — they answer it with a refusal rather than inheriting the wildcard CORS preflight. |
|
|
52
53
|
| 2 | `OPTIONS` | Each auth provider's `handleMetadata` gets a chance (CORS preflight for browser MCP clients); otherwise 204 with MCP CORS. |
|
|
53
54
|
| 3 | `/.well-known/*` | Auth providers' `handleMetadata`, open. 404 when none handles it. |
|
|
@@ -72,7 +73,7 @@ any one file and a reordering reads like a harmless refactor.
|
|
|
72
73
|
([request admission](./request-admission.md)). The permit is held until the
|
|
73
74
|
response *body* completes, not until the handler returns.
|
|
74
75
|
2. **Authorize.** Each `InboundAuth` provider's `authorize` in order, bearer
|
|
75
|
-
before
|
|
76
|
+
before interactive providers. First `ok` admits; if all fail, the last provider's challenge
|
|
76
77
|
response is returned. No providers configured means open — development
|
|
77
78
|
only, and it warns at construction.
|
|
78
79
|
3. **Refuse `?toolkit=`.** Toolkits were removed ([#178](https://github.com/zackbart/connecta/issues/178))
|
|
@@ -116,7 +117,9 @@ The Node-touching paths are `src/node.ts` (the `node:http` adapter),
|
|
|
116
117
|
subpath export — `@zackbart/connecta/node`, `@zackbart/connecta/quickjs` — and
|
|
117
118
|
must stay unreachable from the root entry. The optional Clerk adapter is behind
|
|
118
119
|
`./auth/clerk` for the adjacent reason: `@clerk/backend` is an optional peer,
|
|
119
|
-
not a dependency.
|
|
120
|
+
not a dependency. The zero-dependency Cloudflare Access adapter likewise stays
|
|
121
|
+
behind `./auth/cloudflare-access`: it is Web-API-pure, but its trust contract is
|
|
122
|
+
specific to a direct Worker invocation carrying `ctx.access`.
|
|
120
123
|
|
|
121
124
|
`test/purity.test.ts` walks the relative-import graph from `src/index.ts` and
|
|
122
125
|
fails on (a) any `node:` specifier in a reachable file and (b) the Node
|
|
@@ -156,7 +159,7 @@ src/
|
|
|
156
159
|
operator-ui/ the Preact app, its pure rules, and the built bundle
|
|
157
160
|
connectors/ remote-mcp.ts, api.ts, guarded-fetch.ts
|
|
158
161
|
providers/ the maintained prebuilt connections
|
|
159
|
-
auth/ bearer, clerk (optional peer), downstream OAuth
|
|
162
|
+
auth/ bearer, Cloudflare Access, clerk (optional peer), downstream OAuth
|
|
160
163
|
executors/ the QuickJS pool and child (Node only)
|
|
161
164
|
storage/ memory.ts, file.ts (Node only)
|
|
162
165
|
node.ts listen() + fileStorage re-export (Node only)
|
|
@@ -177,7 +180,7 @@ src/
|
|
|
177
180
|
the connector limiters, then the executor. Node's `listen()` calls it on
|
|
178
181
|
SIGTERM/SIGINT.
|
|
179
182
|
- **Structural mistakes throw at construction.** A duplicate connector id, an
|
|
180
|
-
invalid admission rule, `accessTokens` without
|
|
183
|
+
invalid admission rule, `accessTokens` without an interactive operator provider, a missing
|
|
181
184
|
executor: all refuse to boot. A deployment that starts in the wrong shape is
|
|
182
185
|
worse than one that does not start.
|
|
183
186
|
|
package/documentation/auth.md
CHANGED
|
@@ -1,8 +1,72 @@
|
|
|
1
1
|
# Inbound auth
|
|
2
2
|
|
|
3
3
|
Inbound auth decides who may reach the MCP endpoint. A deployment may admit a
|
|
4
|
-
static bearer, operator-issued access tokens, Clerk identities,
|
|
5
|
-
Static bearers are checked first
|
|
4
|
+
static bearer, operator-issued access tokens, Clerk identities, Cloudflare
|
|
5
|
+
Access identities on Workers, or a mixture. Static bearers are checked first;
|
|
6
|
+
the remaining providers keep configuration order. The first successful
|
|
7
|
+
identity owns the activity actor for that request.
|
|
8
|
+
|
|
9
|
+
## Cloudflare Access on Workers
|
|
10
|
+
|
|
11
|
+
[`cloudflareAccessAuth()`](https://developers.cloudflare.com/workers/configuration/cloudflare-access/)
|
|
12
|
+
is the Worker-specific path:
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { cloudflareAccessAuth } from
|
|
16
|
+
"@zackbart/connecta/auth/cloudflare-access";
|
|
17
|
+
|
|
18
|
+
createConnecta({
|
|
19
|
+
auth: cloudflareAccessAuth(),
|
|
20
|
+
connectors,
|
|
21
|
+
executor,
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The adapter trusts only `ctx.access`, which Cloudflare creates after Access has
|
|
26
|
+
authenticated a request that directly invokes the Worker. It calls
|
|
27
|
+
`ctx.access.getIdentity()` for a human. Cloudflare returns no user identity for
|
|
28
|
+
a service token and strips the service-token headers before invoking the
|
|
29
|
+
Worker, so after `ctx.access` proves admission the adapter uses the Access
|
|
30
|
+
application audience as the automation activity subject. Service tokens on the
|
|
31
|
+
same Access application therefore share attribution. It never reads
|
|
32
|
+
`Cf-Access-Jwt-Assertion`, downloads signing keys, or accepts a JWT from the
|
|
33
|
+
caller. A missing context or an identity lookup that throws fails closed. This
|
|
34
|
+
also means it is deliberately not a Node or `cloudflared` origin adapter, and
|
|
35
|
+
it does not survive a Service Binding hop: those shapes need their own explicit
|
|
36
|
+
trust boundary.
|
|
37
|
+
|
|
38
|
+
A human identity gets MCP and operator access. A Cloudflare service-token
|
|
39
|
+
identity gets MCP access and a stable activity subject, but no `userId`, so it
|
|
40
|
+
cannot write credentials, run downstream OAuth mutations, or issue connecta
|
|
41
|
+
tokens. Access policy decides who reaches the Worker; connecta does not mirror
|
|
42
|
+
email domains, groups, or device posture into a second policy layer.
|
|
43
|
+
|
|
44
|
+
Protect the Worker with a Worker-level Access application whose destination is
|
|
45
|
+
`{ "type": "worker", "worker_id": "<the Worker script tag>" }`. A traditional
|
|
46
|
+
hostname-level application blocks the URL but does not attach `ctx.access` to
|
|
47
|
+
the Worker. Enable [**Managed OAuth**](https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/managed-oauth/)
|
|
48
|
+
on that Worker-level application for interactive MCP clients.
|
|
49
|
+
Cloudflare then owns the unauthenticated challenge and `/.well-known/`
|
|
50
|
+
metadata, issues opaque RFC 8707 tokens, and resolves them into the same trusted
|
|
51
|
+
Worker identity. Do not add a bypass for the discovery routes. A fully
|
|
52
|
+
automated client instead uses a [Cloudflare Access service token](https://developers.cloudflare.com/cloudflare-one/access-controls/service-credentials/service-tokens/)
|
|
53
|
+
through the
|
|
54
|
+
`CF-Access-Client-Id` and `CF-Access-Client-Secret` headers.
|
|
55
|
+
|
|
56
|
+
Worker-level Access runs before every connecta route. Consequently:
|
|
57
|
+
|
|
58
|
+
- `/health`, operator pages, downstream OAuth callbacks, connector-owned
|
|
59
|
+
routes, and `/mcp` all require Access unless a more-specific hostname/path
|
|
60
|
+
policy says otherwise;
|
|
61
|
+
- a static connecta bearer and a `cta_…` token are not standalone edge
|
|
62
|
+
credentials, because Cloudflare rejects them before connecta sees them; and
|
|
63
|
+
- a connector that intentionally exposes a public webhook needs a
|
|
64
|
+
more-specific Access application and bypass policy. Do not bypass connecta's
|
|
65
|
+
OAuth discovery paths when Managed OAuth is enabled.
|
|
66
|
+
|
|
67
|
+
The [Worker example](../examples/worker/) carries the complete deployment shape
|
|
68
|
+
and the [upgrade guide](./upgrading.md#0200--0211) gives the reversible Clerk
|
|
69
|
+
migration.
|
|
6
70
|
|
|
7
71
|
## Clerk configuration is checked at construction
|
|
8
72
|
|
|
@@ -17,7 +81,7 @@ request instead of a base64 stack on every route.
|
|
|
17
81
|
|
|
18
82
|
## Operator-issued access tokens
|
|
19
83
|
|
|
20
|
-
Set `accessTokens: {}` to let eligible
|
|
84
|
+
Set `accessTokens: {}` to let eligible interactive operators create named Bearer
|
|
21
85
|
tokens at `/tokens`:
|
|
22
86
|
|
|
23
87
|
```ts
|
|
@@ -41,7 +105,7 @@ Revoked records remain as metadata tombstones so historical calls keep their
|
|
|
41
105
|
friendly attribution.
|
|
42
106
|
|
|
43
107
|
Access tokens authenticate MCP clients; they are never operator credentials.
|
|
44
|
-
Creation, rename, and revocation require the same eligible
|
|
108
|
+
Creation, rename, and revocation require the same eligible human identity and
|
|
45
109
|
same-origin mutation boundary as connector credentials. `maxActive` defaults
|
|
46
110
|
to 100 and can be set from 1 through 1,000.
|
|
47
111
|
|
|
@@ -51,7 +115,7 @@ effect globally without a convergence window.
|
|
|
51
115
|
|
|
52
116
|
Operator credential mutation is a separate, narrower boundary. The
|
|
53
117
|
`/credentials` shell contains no secret data before authentication, and the
|
|
54
|
-
mutation API requires same-origin requests from an admitted
|
|
118
|
+
mutation API requires same-origin requests from an admitted operator. An MCP
|
|
55
119
|
bearer is never treated as an operator credential, even when it can call every
|
|
56
120
|
connector.
|
|
57
121
|
|
|
@@ -59,10 +123,10 @@ This split is visible in recovery:
|
|
|
59
123
|
|
|
60
124
|
- a bearer-authenticated agent may receive `recovery: "operator_config"` and
|
|
61
125
|
pass its `operatorUrl` to a human;
|
|
62
|
-
-
|
|
126
|
+
- an interactive operator opens that URL, signs in, and updates the
|
|
63
127
|
credential; and
|
|
64
128
|
- a bearer-only deployment still returns the handoff honestly, but mutation
|
|
65
|
-
remains unavailable until
|
|
129
|
+
remains unavailable until interactive operator auth is configured.
|
|
66
130
|
|
|
67
131
|
See [meta-tools](./meta-tools.md#authorization-recovery) for the stable recovery
|
|
68
132
|
envelope and [storage and credentials](./storage-and-credentials.md) for vault
|