@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +3 -1
  3. package/bin/connecta.mjs +23 -6
  4. package/dist/auth/bearer.d.ts +2 -2
  5. package/dist/auth/bearer.js +2 -2
  6. package/dist/auth/clerk.js +1 -0
  7. package/dist/auth/cloudflare-access.d.ts +8 -0
  8. package/dist/auth/cloudflare-access.js +66 -0
  9. package/dist/execute.js +12 -6
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.js +2 -2
  12. package/dist/meta-tools.js +3 -3
  13. package/dist/operator-ui/generated.js +1 -1
  14. package/dist/operator-ui/model.d.ts +3 -3
  15. package/dist/operator-ui/view.d.ts +1 -1
  16. package/dist/operator-ui/view.js +6 -3
  17. package/dist/providers/cloudflare.js +233 -61
  18. package/dist/providers/linear.js +7 -0
  19. package/dist/providers/mixpanel.js +6 -4
  20. package/dist/providers/revenuecat.js +2 -1
  21. package/dist/providers/stripe.js +7 -6
  22. package/dist/routes/access-tokens.d.ts +1 -1
  23. package/dist/routes/access-tokens.js +2 -2
  24. package/dist/routes/activity.js +2 -2
  25. package/dist/routes/credentials.js +1 -1
  26. package/dist/routes/mcp.js +1 -1
  27. package/dist/routes/oauth.js +1 -1
  28. package/dist/routes/shared.d.ts +4 -4
  29. package/dist/routes/shared.js +10 -10
  30. package/dist/routes/ui.js +12 -9
  31. package/dist/skills.d.ts +1 -1
  32. package/dist/skills.js +7 -4
  33. package/dist/types.d.ts +37 -22
  34. package/dist/ui.d.ts +1 -1
  35. package/dist/ui.js +3 -3
  36. package/dist/version.d.ts +1 -1
  37. package/dist/version.js +1 -1
  38. package/documentation/architecture.md +7 -4
  39. package/documentation/auth.md +71 -7
  40. package/documentation/cloudflare.md +19 -1
  41. package/documentation/code-mode.md +4 -4
  42. package/documentation/linear.md +6 -4
  43. package/documentation/meta-tools.md +22 -20
  44. package/documentation/mixpanel.md +5 -3
  45. package/documentation/operations.md +28 -13
  46. package/documentation/operator-ui.md +21 -5
  47. package/documentation/provider-conventions.md +4 -2
  48. package/documentation/revenuecat.md +21 -21
  49. package/documentation/stripe.md +14 -13
  50. package/documentation/upgrading.md +77 -4
  51. package/ethos.md +2 -3
  52. package/examples/worker/README.md +52 -32
  53. package/examples/worker/src/index.ts +32 -38
  54. package/examples/worker/wrangler.jsonc +12 -4
  55. package/package.json +5 -1
  56. package/templates/node/package.json +1 -1
@@ -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 refund rationale: `documentation/stripe.md`. */
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
- ["create_refund", "destructive"],
49
- ["stripe_report", "additive"],
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: \`get_stripe_account_info\` for account information, \`get_balance_summary\` for balances, \`create_refund\` for refunds, \`stripe_report\` for reports. One call instead of three, and a refund named \`create_refund\` reads far more clearly in the approval a human sees than the same refund buried in \`stripe_api_write\` arguments.
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
- * Clerk-only lifecycle for deployment access tokens. The access token itself
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
- * Clerk-only lifecycle for deployment access tokens. The access token itself
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];
@@ -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);
@@ -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
  }
@@ -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);
@@ -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
  } | {
@@ -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.uiAuth?.kind === "clerk"
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 Clerk provider may admit it. A static bearer token is useful
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 Clerk provider gets a turn, the way the /mcp gate does. Stopping at
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.uiAuth?.kind === "clerk");
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 Clerk authentication` }, { status: 403 }),
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 uiAuth = opts.auth.find((provider) => provider.uiAuth)?.uiAuth;
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 eligibleClerkOperator = authz.uiAdminEligible === true;
105
+ const eligibleOperator = authz.uiAdminEligible === true;
103
106
  const credentialManagement = credentialManagementCapability({
104
- eligibleClerkOperator,
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 Clerk is requirednot whether this deployment has opted into
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 = !eligibleClerkOperator
114
- ? "requires_clerk"
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
- eligibleClerkOperator ? opts.credentialVault : undefined, Boolean(opts.activity?.list), credentialManagement, defer, eligibleClerkOperator, opts.discoveryConcurrency, accessTokenManagement);
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. For one read at an unknown address, use search_tools then call_tool; a known address needs only call_tool. For read-only reduction, multiple or dependent calls, loops, joins, or branches, use one execute_code program that discovers, calls, and returns the reduced answer. Only readOnlyHint: true tools run there. Keep unannotated, write-capable, or destructive work top level: search_tools then call_destructive_tool. 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. Fetch skills({ name: \"usage\" }) once for program syntax, selection, repair, examples, and runtime details.";
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. For one read at an unknown address, use search_tools then call_tool; a known address needs only call_tool. For read-only reduction, multiple or dependent calls, loops, joins, or branches, use one execute_code program that discovers, calls, and returns the reduced answer. Only readOnlyHint: true tools run there. Keep unannotated, write-capable, or destructive work top level: search_tools then call_destructive_tool. 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. Fetch skills({ name: "usage" }) once for program syntax, selection, repair, examples, and runtime details.';
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\`. Broad discovery and multi-call work live in a program, not in top-level tools.
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 per task for the program workflow and recovery details below.
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 \`search_tools\`, 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.
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. **Must be an absolute
433
- * `https:` URL** — the value lands in a `<script src>`, so the gate is
434
- * stricter than the branding href gate: no `http:`, no loopback exemption, and
435
- * no root-relative form (a relative path is rejected, not resolved). The
436
- * shipped `clerkAuth` adapter derives this from the publishable key and
437
- * Clerk's Frontend API is always https, so nothing legitimate needs a
438
- * carve-out. A value that fails the gate reaches neither the loader tag nor
439
- * the page's inline auth config: operator pages render without it and report
440
- * that Clerk could not load, and `createConnecta` names the drop in a startup
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 be an
446
- * absolute `https:` URL** — the same gate `frontendApiUrl` passes, because
447
- * this value is where Clerk *navigates* the operator's browser. An Account
448
- * Portal address is always https, so the stricter gate costs nothing real: a
449
- * value that fails it (a `javascript:`/`data:` payload, a cleartext `http:`
450
- * address, a relative path) reaches no part of the page, the shell signs in
451
- * through Clerk's default instead, and `createConnecta` names the drop in a
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, Clerk, ...). */
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 it
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
- eligibleClerkOperator: boolean;
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.eligibleClerkOperator)
212
- return "requires_clerk";
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
- : "requires_clerk", defer, oauthManagement = false, discoveryConcurrency, accessTokenManagement = "not_configured") {
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
@@ -4,4 +4,4 @@
4
4
  * a bump that forgets this file fails the build rather than shipping a stale
5
5
  * version to `/health` and to downstream MCP handshakes.
6
6
  */
7
- export declare const CONNECTA_VERSION = "0.20.0";
7
+ export declare const CONNECTA_VERSION = "0.21.1";
package/dist/version.js CHANGED
@@ -4,4 +4,4 @@
4
4
  * a bump that forgets this file fails the build rather than shipping a stale
5
5
  * version to `/health` and to downstream MCP handshakes.
6
6
  */
7
- export const CONNECTA_VERSION = "0.20.0";
7
+ export const CONNECTA_VERSION = "0.21.1";
@@ -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 Clerk. First `ok` admits; if all fail, the last provider's challenge
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 a Clerk provider, a missing
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
 
@@ -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, or a mixture.
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 Clerk operators create named Bearer
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 Clerk identity and
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 Clerk user. An MCP
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
- - a Clerk-authenticated operator opens that URL, signs in, and updates the
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 Clerk operator auth is configured.
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