@zackbart/connecta 0.19.0 → 0.21.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +3 -1
  3. package/bin/connecta.mjs +23 -6
  4. package/dist/apps-shell.d.ts +10 -12
  5. package/dist/apps-shell.js +29 -220
  6. package/dist/auth/bearer.d.ts +2 -2
  7. package/dist/auth/bearer.js +2 -2
  8. package/dist/auth/clerk.js +1 -0
  9. package/dist/auth/cloudflare-access.d.ts +8 -0
  10. package/dist/auth/cloudflare-access.js +66 -0
  11. package/dist/execute.d.ts +0 -7
  12. package/dist/execute.js +20 -123
  13. package/dist/index.d.ts +2 -2
  14. package/dist/index.js +110 -69
  15. package/dist/invocation.d.ts +1 -1
  16. package/dist/meta-tools.d.ts +0 -1
  17. package/dist/meta-tools.js +10 -495
  18. package/dist/operator-ui/generated.d.ts +2 -2
  19. package/dist/operator-ui/generated.js +1 -1
  20. package/dist/operator-ui/model.d.ts +3 -3
  21. package/dist/operator-ui/view.d.ts +1 -1
  22. package/dist/operator-ui/view.js +6 -3
  23. package/dist/routes/access-tokens.d.ts +1 -1
  24. package/dist/routes/access-tokens.js +2 -2
  25. package/dist/routes/activity.js +2 -2
  26. package/dist/routes/credentials.js +1 -1
  27. package/dist/routes/mcp.js +1 -1
  28. package/dist/routes/oauth.js +1 -1
  29. package/dist/routes/shared.d.ts +4 -4
  30. package/dist/routes/shared.js +10 -10
  31. package/dist/routes/ui.js +12 -9
  32. package/dist/skills.d.ts +1 -1
  33. package/dist/skills.js +11 -8
  34. package/dist/types.d.ts +37 -22
  35. package/dist/ui.d.ts +1 -1
  36. package/dist/ui.js +3 -3
  37. package/dist/version.d.ts +1 -1
  38. package/dist/version.js +1 -1
  39. package/documentation/architecture.md +7 -4
  40. package/documentation/auth.md +71 -7
  41. package/documentation/code-mode.md +23 -23
  42. package/documentation/meta-tools.md +26 -50
  43. package/documentation/operations.md +36 -21
  44. package/documentation/operator-ui.md +21 -5
  45. package/documentation/provider-conventions.md +3 -4
  46. package/documentation/upgrading.md +106 -8
  47. package/ethos.md +3 -4
  48. package/examples/worker/README.md +52 -32
  49. package/examples/worker/src/index.ts +32 -38
  50. package/examples/worker/wrangler.jsonc +12 -4
  51. package/package.json +5 -1
  52. package/templates/node/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,113 @@
2
2
 
3
3
  All notable changes to this package are documented here.
4
4
 
5
+ ## 0.21.0 — 2026-08-28
6
+
7
+ Cloudflare Access becomes the canonical interactive-auth path for Worker
8
+ deployments. It authenticates both MCP clients and human operators before the
9
+ Worker runs, while connecta consumes only the trusted runtime identity. Clerk
10
+ is unchanged and remains supported: existing deployments can add Access,
11
+ verify the edge cutover, and remove Clerk later, with no storage migration or
12
+ token conversion. Node deployments can ignore this release beyond the version
13
+ pin.
14
+
15
+ ### Added
16
+
17
+ - **Direct Worker Access auth.** `cloudflareAccessAuth()` ships from
18
+ `@zackbart/connecta/auth/cloudflare-access` with no dependency and no JWT
19
+ verifier. Human `ctx.access` identities may use MCP and operator routes;
20
+ service-token identities may use MCP but cannot mutate operator state. Since
21
+ Cloudflare exposes no user identity for a service token, its Access
22
+ application audience is the shared activity subject. The same suite runs
23
+ under Node and workerd (#506).
24
+ - **Ambient operator sessions.** The operator shell selects Cloudflare Access
25
+ when the current invocation carries it, sends no browser-readable token, and
26
+ signs out through Cloudflare. A co-configured Clerk provider remains the
27
+ shell before Access is attached and the rollback path after it is detached
28
+ (#506).
29
+ - **Access-aware doctor.** `connecta doctor` accepts
30
+ `CF_ACCESS_CLIENT_ID`/`CF_ACCESS_CLIENT_SECRET` and sends the pair to health
31
+ and MCP requests. A partial pair fails before network access (#506).
32
+
33
+ ### Changed
34
+
35
+ - **Cold reads stay in code mode.** One known canonical address still uses
36
+ `call_tool`, while an unknown-address read now starts with one
37
+ `execute_code` program and keeps discovery results off the model-facing
38
+ route. The current-version benchmark is reset to four deterministic
39
+ whole-agent cases covering both routes, exact provider semantics, private
40
+ pagination, forwarding bytes, tokens, and latency.
41
+ - **Worker deployment path.** The shipped Worker example uses Access and
42
+ Managed OAuth through a Worker-level `worker` destination, carries a local
43
+ `access.dev` identity, and documents service tokens for unattended callers.
44
+ A hostname-only Access application gates the URL but does not supply
45
+ `ctx.access`. The Clerk shape stays beside the provider as the reversible
46
+ migration seam. Static connecta and operator-issued bearers remain supported
47
+ by core but are not standalone credentials through a whole-Worker Access
48
+ gate (#506).
49
+ - **Operator capability is vendor-neutral.** Inbound auth providers now declare
50
+ interactive-operator capability explicitly, and runtime context reaches
51
+ their authorization hook as an optional third argument. Existing custom
52
+ providers with the two-argument hook remain source-compatible (#506).
53
+
54
+ ## 0.20.0 — 2026-08-26
55
+
56
+ This release removes the two side languages that had grown around the seven
57
+ tools: direct-call field projection and live reads from program-rendered HTML.
58
+ Agents now shape data in JavaScript, page a genuinely large direct result with
59
+ `get_result`, and render a display-only view. Configuration becomes strict at
60
+ the same boundary, so a typo fails at startup instead of surviving as inert
61
+ deployment state. Existing deployments using current documented options need
62
+ only bump the pin and reconcile generation B; callers that send `fields`,
63
+ programs that bind UI reads, and JavaScript deployments carrying retired or
64
+ misspelled options must migrate. The implementation stack deletes 2,866 lines
65
+ and adds 1,063 before these release notes, while preserving exactly seven MCP
66
+ tools and the Node and Worker deployment shapes.
67
+
68
+ ### Changed
69
+
70
+ - **One data-shaping language.** `call_tool` and `call_destructive_tool` no
71
+ longer advertise or accept `fields`; the path resolver, schema walker,
72
+ projection recovery records, guidance, and tests are gone. `execute_code`
73
+ remains the projection surface, while `get_result` still pages oversized
74
+ direct reads byte-exactly. The 21-scenario current-version audit passes, fixed
75
+ tool definitions fall from 1,625 to 1,587 tokens, and a deterministic
76
+ 52,396-byte document succeeds 3/3 through `call_tool` then `get_result`
77
+ (#482).
78
+ - **Display-only program UI.** `connecta.ui(html)` keeps success-only Apps
79
+ delivery, sandboxing, one shared rich-output budget, executor parity, and the
80
+ compact mirrored return. Its read manifest, host-call bridge, and second
81
+ argument are removed. All seven tools explicitly remain model-only, and only
82
+ `execute_code` advertises the cache-busted v3 display resource, so neither a
83
+ current nor cached shell can call Connecta tools (#484).
84
+ - **Strict configuration.** `createConnecta` rejects unknown own options at
85
+ every closed configuration path without reading or quoting their values.
86
+ Each closed schema is compile-time exhaustive against its public type;
87
+ connector, auth, storage, activity-store, logger, deployment-metadata, and
88
+ executor implementations remain open leaves (#485).
89
+ - **Smaller package and tests.** Generated operator assets publish explicit
90
+ 95-byte string declarations instead of embedding about 95 KB of literals,
91
+ with byte-identical runtime output and a packed-package size guard (#486).
92
+ The remaining copied UI credential and meta-tool fixtures are shared without
93
+ changing any test or assertion count, removing another 127 repository lines
94
+ (#479).
95
+
96
+ ### Removed
97
+
98
+ - The direct-call `fields` projection option and its dot/array path language.
99
+ - Program UI read bindings, `connecta.read`, and the Apps-to-host tool-call
100
+ bridge.
101
+
102
+ ### Declined with evidence
103
+
104
+ - Discovery filtering did not ship: its sealed qualification retained complete
105
+ scenario coverage but missed top-1, recall, default-page recall, and negative
106
+ false-positive gates. The holdout cases were not inspected or tuned (#481).
107
+ - Lean object-result text did not ship: Codex and Claude read ordinary
108
+ structured values, but Claude's error route took extra recovery actions and
109
+ Cursor was unavailable for the required matrix. The compatibility JSON copy
110
+ remains (#483).
111
+
5
112
  ## 0.19.0 — 2026-08-25
6
113
 
7
114
  This release is a smaller, simpler package with no behavioral change for a
package/README.md CHANGED
@@ -78,7 +78,9 @@ Fifty issues in, one small object out. Your context window notices.
78
78
 
79
79
  There is also an operator surface, off until you turn it on: sign-in, an
80
80
  encrypted credential vault with rotation, revocable per-client tokens, and a
81
- payload-free activity log.
81
+ payload-free activity log. Worker deployments can use Cloudflare Access for
82
+ both MCP and operator identity; Node deployments and existing Workers can use
83
+ Clerk.
82
84
 
83
85
  Connecta is not a platform, a marketplace, a policy engine, or a multi-tenant
84
86
  service. Those are decisions, and the [ethos](./ethos.md) records each one
package/bin/connecta.mjs CHANGED
@@ -27,7 +27,8 @@ function shellCd(path) {
27
27
  function usage() {
28
28
  console.log(`Usage:
29
29
  connecta init [directory]
30
- CONNECTA_TOKEN=<bearer> connecta doctor [--url http://localhost:8787]`);
30
+ CONNECTA_TOKEN=<bearer> connecta doctor [--url http://localhost:8787]
31
+ CF_ACCESS_CLIENT_ID=<id> CF_ACCESS_CLIENT_SECRET=<secret> connecta doctor --url https://worker.example`);
31
32
  }
32
33
 
33
34
  async function init() {
@@ -169,19 +170,35 @@ async function doctor() {
169
170
  !loopbackHosts.has(parsedUrl.hostname)
170
171
  ) {
171
172
  throw new Error(
172
- "Refusing to send a bearer token over remote plaintext HTTP. Use HTTPS.",
173
+ "Refusing to send authentication credentials over remote plaintext HTTP. Use HTTPS.",
173
174
  );
174
175
  }
175
176
  const baseUrl = requestedUrl.replace(/\/+$/, "");
176
177
  const token = process.env.CONNECTA_TOKEN;
177
- if (!token) {
178
+ const accessClientId = process.env.CF_ACCESS_CLIENT_ID;
179
+ const accessClientSecret = process.env.CF_ACCESS_CLIENT_SECRET;
180
+ if (Boolean(accessClientId) !== Boolean(accessClientSecret)) {
178
181
  throw new Error(
179
- "Set CONNECTA_TOKEN so doctor can inspect the MCP surface.",
182
+ "Set both CF_ACCESS_CLIENT_ID and CF_ACCESS_CLIENT_SECRET.",
180
183
  );
181
184
  }
185
+ if (!token && !accessClientId) {
186
+ throw new Error(
187
+ "Set CONNECTA_TOKEN or a CF_ACCESS_CLIENT_ID/CF_ACCESS_CLIENT_SECRET pair so doctor can inspect the MCP surface.",
188
+ );
189
+ }
190
+ const authHeaders = {
191
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
192
+ ...(accessClientId && accessClientSecret
193
+ ? {
194
+ "CF-Access-Client-Id": accessClientId,
195
+ "CF-Access-Client-Secret": accessClientSecret,
196
+ }
197
+ : {}),
198
+ };
182
199
 
183
200
  const health = await jsonResponse(
184
- await doctorFetch(`${baseUrl}/health`),
201
+ await doctorFetch(`${baseUrl}/health`, { headers: authHeaders }),
185
202
  );
186
203
  if (health.status !== "ok") {
187
204
  throw new Error(`Unexpected health status: ${String(health.status)}`);
@@ -206,7 +223,7 @@ async function doctor() {
206
223
  await doctorFetch(`${baseUrl}/mcp`, {
207
224
  method: "POST",
208
225
  headers: {
209
- Authorization: `Bearer ${token}`,
226
+ ...authHeaders,
210
227
  "Content-Type": "application/json",
211
228
  Accept: "application/json, text/event-stream",
212
229
  },
@@ -3,16 +3,16 @@
3
3
  *
4
4
  * A build-time string constant, not a file read at startup: the core is
5
5
  * Web-API-only so it runs unchanged on Workers, and the same bytes have to
6
- * serve everywhere. It renders whatever HTML a program handed `connecta.ui`
7
- * inside a nested `srcdoc` frame. The one-argument form forwards no channel;
8
- * an explicitly bound view gets only named read calls through the trusted
9
- * shell, never a raw host channel.
6
+ * serve everywhere. The shell is display-only: it renders whatever HTML a
7
+ * program handed `connecta.ui` inside a nested `srcdoc` frame and forwards no
8
+ * channel back from that frame to the host, so program-authored markup is
9
+ * inert beyond its own pixels.
10
10
  *
11
11
  * The address carries a version segment because hosts are permitted to
12
12
  * prefetch and cache templates by URI: change these bytes, bump the version.
13
13
  */
14
14
  /** The only `ui://` URI in the system. No program input reaches it. */
15
- export declare const PROGRAM_UI_RESOURCE_URI = "ui://connecta/program-ui/v2";
15
+ export declare const PROGRAM_UI_RESOURCE_URI = "ui://connecta/program-ui/v3";
16
16
  /** The mimeType the Apps spec requires of an HTML template. */
17
17
  export declare const PROGRAM_UI_MIME_TYPE = "text/html;profile=mcp-app";
18
18
  /**
@@ -29,11 +29,9 @@ export declare const MCP_APPS_EXTENSION = "io.modelcontextprotocol/ui";
29
29
  * Apps postMessage dialect (`ui/initialize`, `ui/notifications/initialized`,
30
30
  * `ui/notifications/tool-result`, `ui/notifications/size-changed`,
31
31
  * `ui/resource-teardown`), lifts `_meta["connecta/ui"].html` out of the
32
- * delivered tool result, and puts it in a frame. An optional read manifest
33
- * installs one narrow `connecta.read(name, args)` bridge in that inner frame;
34
- * the outer shell maps declared names to the existing `call_tool` meta-tool.
35
- * It declares no CSP domains, so the host applies its restrictive default and
36
- * the `srcdoc` frame inherits `default-src 'none'` — program markup still gets
37
- * no direct network.
32
+ * delivered tool result, and puts it in a frame. It declares no CSP domains,
33
+ * so the host applies its restrictive default and the `srcdoc` frame inherits
34
+ * `default-src 'none'`. The payload gets scripts and local interactivity, and
35
+ * no network.
38
36
  */
39
- export declare const PROGRAM_UI_SHELL_HTML = "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>connecta program view</title>\n <style>\n html,\n body {\n margin: 0;\n padding: 0;\n background: transparent;\n }\n #program-view {\n display: block;\n width: 100%;\n min-height: 480px;\n border: 0;\n }\n </style>\n </head>\n <body>\n <iframe\n id=\"program-view\"\n title=\"Program-rendered view\"\n sandbox=\"allow-scripts\"\n srcdoc=\"\"\n ></iframe>\n <script>\n (function () {\n \"use strict\";\n // The outer shell is the only host peer. The payload frame is\n // sandboxed to scripts alone, with no same-origin escape. Its one\n // optional message dialect is handled below and translated into\n // bounded call_tool requests; raw JSON-RPC is never forwarded.\n var host = window.parent;\n var view = document.getElementById(\"program-view\");\n var initializeId = \"connecta-ui-initialize\";\n var lastWidth = 0;\n var lastHeight = 0;\n var reads = null;\n var hostCanCallTools = false;\n var nextHostRequestId = 0;\n var pendingHostReads = Object.create(null);\n var activeHostReads = 0;\n var maxActiveHostReads = 8;\n\n function send(message) {\n if (!host || host === window) return;\n host.postMessage(message, \"*\");\n }\n\n function notify(method, params) {\n send({ jsonrpc: \"2.0\", method: method, params: params });\n }\n\n // Program views are fixed-height by construction. The shell has no\n // content-height bridge to the payload frame, so it can never learn\n // the payload's content height, and\n // what it reports here is its own box: the min-height above, unless\n // the host has given it more. Taller content scrolls inside the inner\n // frame rather than growing the view. Raising the min-height is the\n // only lever; a content-height signal would cost the isolation.\n function reportSize() {\n var width = Math.ceil(document.documentElement.clientWidth);\n var height = Math.ceil(document.documentElement.scrollHeight);\n if (width === lastWidth && height === lastHeight) return;\n lastWidth = width;\n lastHeight = height;\n notify(\"ui/notifications/size-changed\", {\n width: width,\n height: height\n });\n }\n\n function payload(result) {\n if (!result || typeof result !== \"object\") return null;\n var meta = result._meta;\n if (!meta || typeof meta !== \"object\") return null;\n var value = meta[\"connecta/ui\"];\n if (!value || typeof value !== \"object\") return null;\n return typeof value.html === \"string\" && value.html.length > 0\n ? value\n : null;\n }\n\n // This function is serialized into the opaque-origin payload frame.\n // It knows no addresses and has no host channel of its own: one named\n // read request goes to the trusted outer shell and one correlated\n // result comes back.\n function payloadReadBridge() {\n \"use strict\";\n var pending = Object.create(null);\n var nextId = 0;\n\n function read(name, args) {\n return new Promise(function (resolve, reject) {\n var id = String(++nextId);\n pending[id] = { resolve: resolve, reject: reject };\n try {\n window.parent.postMessage({\n type: \"connecta/read\",\n id: id,\n name: name,\n args: args === undefined ? {} : args\n }, \"*\");\n } catch (error) {\n delete pending[id];\n reject(error);\n }\n });\n }\n\n Object.defineProperty(globalThis, \"connecta\", {\n value: Object.freeze({ read: read }),\n configurable: false,\n enumerable: true,\n writable: false\n });\n\n window.addEventListener(\"message\", function (event) {\n if (event.source !== window.parent) return;\n var message = event.data;\n if (!message || message.type !== \"connecta/read-result\") return;\n var waiter = pending[message.id];\n if (!waiter) return;\n delete pending[message.id];\n if (message.ok) waiter.resolve(message.value);\n else waiter.reject(new Error(message.error || \"Read failed\"));\n });\n }\n\n function htmlWithReadBridge(html) {\n var script =\n \"<scr\" + \"ipt>(\" + payloadReadBridge.toString() + \")();</scr\" + \"ipt>\";\n var head = /<head(?:\\s[^>]*)?>/i.exec(html);\n if (head) {\n var at = (head.index || 0) + head[0].length;\n return html.slice(0, at) + script + html.slice(at);\n }\n var document = /<html(?:\\s[^>]*)?>/i.exec(html);\n if (document) {\n var afterHtml = (document.index || 0) + document[0].length;\n return html.slice(0, afterHtml) + \"<head>\" + script + \"</head>\" + html.slice(afterHtml);\n }\n return script + html;\n }\n\n function render(params) {\n var value =\n payload(params) ||\n payload(params && params.result) ||\n payload(params && params.toolResult);\n if (value === null) return;\n reads = value.reads && typeof value.reads === \"object\"\n ? value.reads\n : null;\n view.srcdoc = reads\n ? htmlWithReadBridge(value.html)\n : value.html;\n reportSize();\n }\n\n function readError(message, fallback) {\n if (message && typeof message.message === \"string\") return message.message;\n if (message && message.data && typeof message.data.message === \"string\") {\n return message.data.message;\n }\n return fallback;\n }\n\n function finishInnerRead(innerId, ok, value) {\n if (!view.contentWindow) return;\n view.contentWindow.postMessage(ok\n ? { type: \"connecta/read-result\", id: innerId, ok: true, value: value }\n : { type: \"connecta/read-result\", id: innerId, ok: false, error: value }, \"*\");\n }\n\n function beginInnerRead(message) {\n if (!hostCanCallTools) {\n finishInnerRead(message && message.id, false, \"This host does not support app-initiated server tool calls\");\n return;\n }\n if (!message || typeof message.id !== \"string\" || typeof message.name !== \"string\") return;\n if (!reads || !Object.prototype.hasOwnProperty.call(reads, message.name)) {\n finishInnerRead(message.id, false, \"Unknown read binding\");\n return;\n }\n if (activeHostReads >= maxActiveHostReads) {\n finishInnerRead(message.id, false, \"Too many concurrent reads\");\n return;\n }\n var binding = reads[message.name];\n if (!binding || typeof binding !== \"object\" || typeof binding.address !== \"string\") {\n finishInnerRead(message.id, false, \"Invalid read binding\");\n return;\n }\n var supplied = message.args;\n if (!supplied || typeof supplied !== \"object\" || Array.isArray(supplied)) {\n finishInnerRead(message.id, false, \"Read arguments must be an object\");\n return;\n }\n var allowed = Array.isArray(binding.viewArgs) ? binding.viewArgs : [];\n var suppliedKeys = Object.keys(supplied);\n for (var i = 0; i < suppliedKeys.length; i++) {\n var key = suppliedKeys[i];\n if (allowed.indexOf(key) === -1) {\n finishInnerRead(message.id, false, \"Undeclared read argument \" + JSON.stringify(key));\n return;\n }\n }\n var args = Object.create(null);\n var fixed = binding.fixedArgs && typeof binding.fixedArgs === \"object\"\n ? binding.fixedArgs\n : {};\n Object.keys(fixed).forEach(function (key) { args[key] = fixed[key]; });\n suppliedKeys.forEach(function (key) { args[key] = supplied[key]; });\n\n var hostId = \"connecta-ui-read-\" + String(++nextHostRequestId);\n pendingHostReads[hostId] = { innerId: message.id };\n activeHostReads++;\n send({\n jsonrpc: \"2.0\",\n id: hostId,\n method: \"tools/call\",\n params: {\n name: \"call_tool\",\n arguments: {\n address: binding.address,\n args: args,\n resultMode: \"value\"\n }\n }\n });\n }\n\n function finishHostRead(message) {\n var pending = pendingHostReads[message.id];\n if (!pending) return false;\n delete pendingHostReads[message.id];\n activeHostReads--;\n if (message.error) {\n finishInnerRead(pending.innerId, false, readError(message.error, \"Host rejected read\"));\n return true;\n }\n var toolResult = message.result;\n if (!toolResult || typeof toolResult !== \"object\") {\n finishInnerRead(pending.innerId, false, \"Host returned an invalid tool result\");\n return true;\n }\n var structured = toolResult.structuredContent;\n if (toolResult.isError || (structured && structured.ok === false)) {\n var detail = structured && structured.error;\n var content = Array.isArray(toolResult.content)\n ? toolResult.content.find(function (block) { return block && block.type === \"text\"; })\n : null;\n finishInnerRead(\n pending.innerId,\n false,\n readError(detail, content && content.text ? content.text : \"Read failed\")\n );\n return true;\n }\n var value = structured && structured.ok === true &&\n Object.prototype.hasOwnProperty.call(structured, \"data\")\n ? structured.data\n : structured !== undefined\n ? structured\n : toolResult;\n finishInnerRead(pending.innerId, true, value);\n return true;\n }\n\n window.addEventListener(\"message\", function (event) {\n var message = event.data;\n if (event.source === view.contentWindow) {\n if (message && message.type === \"connecta/read\") beginInnerRead(message);\n return;\n }\n if (event.source !== host) return;\n if (!message || message.jsonrpc !== \"2.0\") return;\n if (message.id !== undefined && finishHostRead(message)) return;\n if (message.method === \"ui/notifications/tool-result\") {\n render(message.params);\n return;\n }\n if (message.method === \"ui/resource-teardown\") {\n // A host->view request, not a notification: the host waits for\n // this reply before it tears the view down. There is nothing to\n // release, so answer immediately rather than make it time out.\n if (message.id !== undefined && message.id !== null) {\n send({ jsonrpc: \"2.0\", id: message.id, result: {} });\n }\n return;\n }\n // Only a completed handshake earns \"initialized\". A JSON-RPC error\n // response carries the same id, and announcing initialization on one\n // would assert a handshake that never happened.\n if (message.id === initializeId && message.result !== undefined) {\n var capabilities = message.result.hostCapabilities;\n hostCanCallTools = Boolean(capabilities && capabilities.serverTools);\n notify(\"ui/notifications/initialized\", {});\n }\n });\n\n window.addEventListener(\"resize\", reportSize);\n view.addEventListener(\"load\", reportSize);\n\n // Every field here is required by the Apps initialize schema, and a\n // conforming host rejects the request outright when one is missing \u2014\n // which would strand the shell before any tool result arrives.\n send({\n jsonrpc: \"2.0\",\n id: initializeId,\n method: \"ui/initialize\",\n params: {\n appInfo: { name: \"connecta program view\", version: \"1\" },\n appCapabilities: {},\n protocolVersion: \"2026-01-26\"\n }\n });\n reportSize();\n })();\n </script>\n </body>\n</html>\n";
37
+ export declare const PROGRAM_UI_SHELL_HTML = "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>connecta program view</title>\n <style>\n html,\n body {\n margin: 0;\n padding: 0;\n background: transparent;\n }\n #program-view {\n display: block;\n width: 100%;\n min-height: 480px;\n border: 0;\n }\n </style>\n </head>\n <body>\n <iframe\n id=\"program-view\"\n title=\"Program-rendered view\"\n sandbox=\"allow-scripts\"\n srcdoc=\"\"\n ></iframe>\n <script>\n (function () {\n \"use strict\";\n // The host frame is the only peer this shell speaks to, in either\n // direction. The payload frame below is sandboxed to scripts alone,\n // with no same-origin escape, and is never handed a reply path:\n // anything it posts fails the source check and is dropped. There is\n // no bridge from program HTML to the host, by construction rather\n // than by validation.\n var host = window.parent;\n var view = document.getElementById(\"program-view\");\n var initializeId = \"connecta-ui-initialize\";\n var lastWidth = 0;\n var lastHeight = 0;\n\n function send(message) {\n if (!host || host === window) return;\n host.postMessage(message, \"*\");\n }\n\n function notify(method, params) {\n send({ jsonrpc: \"2.0\", method: method, params: params });\n }\n\n // Program views are fixed-height by construction. The shell has no\n // bridge to the payload frame \u2014 that is the security posture, not an\n // omission \u2014 so it can never learn the payload's content height, and\n // what it reports here is its own box: the min-height above, unless\n // the host has given it more. Taller content scrolls inside the inner\n // frame rather than growing the view. Raising the min-height is the\n // only lever; a content-height signal would cost the isolation.\n function reportSize() {\n var width = Math.ceil(document.documentElement.clientWidth);\n var height = Math.ceil(document.documentElement.scrollHeight);\n if (width === lastWidth && height === lastHeight) return;\n lastWidth = width;\n lastHeight = height;\n notify(\"ui/notifications/size-changed\", {\n width: width,\n height: height\n });\n }\n\n function payloadHtml(result) {\n if (!result || typeof result !== \"object\") return null;\n var meta = result._meta;\n if (!meta || typeof meta !== \"object\") return null;\n var payload = meta[\"connecta/ui\"];\n if (!payload || typeof payload !== \"object\") return null;\n var html = payload.html;\n return typeof html === \"string\" && html.length > 0 ? html : null;\n }\n\n function render(params) {\n var html =\n payloadHtml(params) ||\n payloadHtml(params && params.result) ||\n payloadHtml(params && params.toolResult);\n if (html === null) return;\n view.srcdoc = html;\n reportSize();\n }\n\n window.addEventListener(\"message\", function (event) {\n if (event.source !== host) return;\n var message = event.data;\n if (!message || message.jsonrpc !== \"2.0\") return;\n if (message.method === \"ui/notifications/tool-result\") {\n render(message.params);\n return;\n }\n if (message.method === \"ui/resource-teardown\") {\n // A host->view request, not a notification: the host waits for\n // this reply before it tears the view down. There is nothing to\n // release, so answer immediately rather than make it time out.\n if (message.id !== undefined && message.id !== null) {\n send({ jsonrpc: \"2.0\", id: message.id, result: {} });\n }\n return;\n }\n // Only a completed handshake earns \"initialized\". A JSON-RPC error\n // response carries the same id, and announcing initialization on one\n // would assert a handshake that never happened.\n if (message.id === initializeId && message.result !== undefined) {\n notify(\"ui/notifications/initialized\", {});\n }\n });\n\n window.addEventListener(\"resize\", reportSize);\n view.addEventListener(\"load\", reportSize);\n\n // Every field here is required by the Apps initialize schema, and a\n // conforming host rejects the request outright when one is missing \u2014\n // which would strand the shell before any tool result arrives.\n send({\n jsonrpc: \"2.0\",\n id: initializeId,\n method: \"ui/initialize\",\n params: {\n appInfo: { name: \"connecta program view\", version: \"1\" },\n appCapabilities: {},\n protocolVersion: \"2026-01-26\"\n }\n });\n reportSize();\n })();\n </script>\n </body>\n</html>\n";
@@ -3,16 +3,16 @@
3
3
  *
4
4
  * A build-time string constant, not a file read at startup: the core is
5
5
  * Web-API-only so it runs unchanged on Workers, and the same bytes have to
6
- * serve everywhere. It renders whatever HTML a program handed `connecta.ui`
7
- * inside a nested `srcdoc` frame. The one-argument form forwards no channel;
8
- * an explicitly bound view gets only named read calls through the trusted
9
- * shell, never a raw host channel.
6
+ * serve everywhere. The shell is display-only: it renders whatever HTML a
7
+ * program handed `connecta.ui` inside a nested `srcdoc` frame and forwards no
8
+ * channel back from that frame to the host, so program-authored markup is
9
+ * inert beyond its own pixels.
10
10
  *
11
11
  * The address carries a version segment because hosts are permitted to
12
12
  * prefetch and cache templates by URI: change these bytes, bump the version.
13
13
  */
14
14
  /** The only `ui://` URI in the system. No program input reaches it. */
15
- export const PROGRAM_UI_RESOURCE_URI = "ui://connecta/program-ui/v2";
15
+ export const PROGRAM_UI_RESOURCE_URI = "ui://connecta/program-ui/v3";
16
16
  /** The mimeType the Apps spec requires of an HTML template. */
17
17
  export const PROGRAM_UI_MIME_TYPE = "text/html;profile=mcp-app";
18
18
  /**
@@ -29,12 +29,10 @@ export const MCP_APPS_EXTENSION = "io.modelcontextprotocol/ui";
29
29
  * Apps postMessage dialect (`ui/initialize`, `ui/notifications/initialized`,
30
30
  * `ui/notifications/tool-result`, `ui/notifications/size-changed`,
31
31
  * `ui/resource-teardown`), lifts `_meta["connecta/ui"].html` out of the
32
- * delivered tool result, and puts it in a frame. An optional read manifest
33
- * installs one narrow `connecta.read(name, args)` bridge in that inner frame;
34
- * the outer shell maps declared names to the existing `call_tool` meta-tool.
35
- * It declares no CSP domains, so the host applies its restrictive default and
36
- * the `srcdoc` frame inherits `default-src 'none'` — program markup still gets
37
- * no direct network.
32
+ * delivered tool result, and puts it in a frame. It declares no CSP domains,
33
+ * so the host applies its restrictive default and the `srcdoc` frame inherits
34
+ * `default-src 'none'`. The payload gets scripts and local interactivity, and
35
+ * no network.
38
36
  */
39
37
  export const PROGRAM_UI_SHELL_HTML = `<!doctype html>
40
38
  <html lang="en">
@@ -67,21 +65,17 @@ export const PROGRAM_UI_SHELL_HTML = `<!doctype html>
67
65
  <script>
68
66
  (function () {
69
67
  "use strict";
70
- // The outer shell is the only host peer. The payload frame is
71
- // sandboxed to scripts alone, with no same-origin escape. Its one
72
- // optional message dialect is handled below and translated into
73
- // bounded call_tool requests; raw JSON-RPC is never forwarded.
68
+ // The host frame is the only peer this shell speaks to, in either
69
+ // direction. The payload frame below is sandboxed to scripts alone,
70
+ // with no same-origin escape, and is never handed a reply path:
71
+ // anything it posts fails the source check and is dropped. There is
72
+ // no bridge from program HTML to the host, by construction rather
73
+ // than by validation.
74
74
  var host = window.parent;
75
75
  var view = document.getElementById("program-view");
76
76
  var initializeId = "connecta-ui-initialize";
77
77
  var lastWidth = 0;
78
78
  var lastHeight = 0;
79
- var reads = null;
80
- var hostCanCallTools = false;
81
- var nextHostRequestId = 0;
82
- var pendingHostReads = Object.create(null);
83
- var activeHostReads = 0;
84
- var maxActiveHostReads = 8;
85
79
 
86
80
  function send(message) {
87
81
  if (!host || host === window) return;
@@ -93,8 +87,8 @@ export const PROGRAM_UI_SHELL_HTML = `<!doctype html>
93
87
  }
94
88
 
95
89
  // Program views are fixed-height by construction. The shell has no
96
- // content-height bridge to the payload frame, so it can never learn
97
- // the payload's content height, and
90
+ // bridge to the payload frame that is the security posture, not an
91
+ // omission — so it can never learn the payload's content height, and
98
92
  // what it reports here is its own box: the min-height above, unless
99
93
  // the host has given it more. Taller content scrolls inside the inner
100
94
  // frame rather than growing the view. Raising the min-height is the
@@ -111,213 +105,30 @@ export const PROGRAM_UI_SHELL_HTML = `<!doctype html>
111
105
  });
112
106
  }
113
107
 
114
- function payload(result) {
108
+ function payloadHtml(result) {
115
109
  if (!result || typeof result !== "object") return null;
116
110
  var meta = result._meta;
117
111
  if (!meta || typeof meta !== "object") return null;
118
- var value = meta["connecta/ui"];
119
- if (!value || typeof value !== "object") return null;
120
- return typeof value.html === "string" && value.html.length > 0
121
- ? value
122
- : null;
123
- }
124
-
125
- // This function is serialized into the opaque-origin payload frame.
126
- // It knows no addresses and has no host channel of its own: one named
127
- // read request goes to the trusted outer shell and one correlated
128
- // result comes back.
129
- function payloadReadBridge() {
130
- "use strict";
131
- var pending = Object.create(null);
132
- var nextId = 0;
133
-
134
- function read(name, args) {
135
- return new Promise(function (resolve, reject) {
136
- var id = String(++nextId);
137
- pending[id] = { resolve: resolve, reject: reject };
138
- try {
139
- window.parent.postMessage({
140
- type: "connecta/read",
141
- id: id,
142
- name: name,
143
- args: args === undefined ? {} : args
144
- }, "*");
145
- } catch (error) {
146
- delete pending[id];
147
- reject(error);
148
- }
149
- });
150
- }
151
-
152
- Object.defineProperty(globalThis, "connecta", {
153
- value: Object.freeze({ read: read }),
154
- configurable: false,
155
- enumerable: true,
156
- writable: false
157
- });
158
-
159
- window.addEventListener("message", function (event) {
160
- if (event.source !== window.parent) return;
161
- var message = event.data;
162
- if (!message || message.type !== "connecta/read-result") return;
163
- var waiter = pending[message.id];
164
- if (!waiter) return;
165
- delete pending[message.id];
166
- if (message.ok) waiter.resolve(message.value);
167
- else waiter.reject(new Error(message.error || "Read failed"));
168
- });
169
- }
170
-
171
- function htmlWithReadBridge(html) {
172
- var script =
173
- "<scr" + "ipt>(" + payloadReadBridge.toString() + ")();</scr" + "ipt>";
174
- var head = /<head(?:\\s[^>]*)?>/i.exec(html);
175
- if (head) {
176
- var at = (head.index || 0) + head[0].length;
177
- return html.slice(0, at) + script + html.slice(at);
178
- }
179
- var document = /<html(?:\\s[^>]*)?>/i.exec(html);
180
- if (document) {
181
- var afterHtml = (document.index || 0) + document[0].length;
182
- return html.slice(0, afterHtml) + "<head>" + script + "</head>" + html.slice(afterHtml);
183
- }
184
- return script + html;
112
+ var payload = meta["connecta/ui"];
113
+ if (!payload || typeof payload !== "object") return null;
114
+ var html = payload.html;
115
+ return typeof html === "string" && html.length > 0 ? html : null;
185
116
  }
186
117
 
187
118
  function render(params) {
188
- var value =
189
- payload(params) ||
190
- payload(params && params.result) ||
191
- payload(params && params.toolResult);
192
- if (value === null) return;
193
- reads = value.reads && typeof value.reads === "object"
194
- ? value.reads
195
- : null;
196
- view.srcdoc = reads
197
- ? htmlWithReadBridge(value.html)
198
- : value.html;
119
+ var html =
120
+ payloadHtml(params) ||
121
+ payloadHtml(params && params.result) ||
122
+ payloadHtml(params && params.toolResult);
123
+ if (html === null) return;
124
+ view.srcdoc = html;
199
125
  reportSize();
200
126
  }
201
127
 
202
- function readError(message, fallback) {
203
- if (message && typeof message.message === "string") return message.message;
204
- if (message && message.data && typeof message.data.message === "string") {
205
- return message.data.message;
206
- }
207
- return fallback;
208
- }
209
-
210
- function finishInnerRead(innerId, ok, value) {
211
- if (!view.contentWindow) return;
212
- view.contentWindow.postMessage(ok
213
- ? { type: "connecta/read-result", id: innerId, ok: true, value: value }
214
- : { type: "connecta/read-result", id: innerId, ok: false, error: value }, "*");
215
- }
216
-
217
- function beginInnerRead(message) {
218
- if (!hostCanCallTools) {
219
- finishInnerRead(message && message.id, false, "This host does not support app-initiated server tool calls");
220
- return;
221
- }
222
- if (!message || typeof message.id !== "string" || typeof message.name !== "string") return;
223
- if (!reads || !Object.prototype.hasOwnProperty.call(reads, message.name)) {
224
- finishInnerRead(message.id, false, "Unknown read binding");
225
- return;
226
- }
227
- if (activeHostReads >= maxActiveHostReads) {
228
- finishInnerRead(message.id, false, "Too many concurrent reads");
229
- return;
230
- }
231
- var binding = reads[message.name];
232
- if (!binding || typeof binding !== "object" || typeof binding.address !== "string") {
233
- finishInnerRead(message.id, false, "Invalid read binding");
234
- return;
235
- }
236
- var supplied = message.args;
237
- if (!supplied || typeof supplied !== "object" || Array.isArray(supplied)) {
238
- finishInnerRead(message.id, false, "Read arguments must be an object");
239
- return;
240
- }
241
- var allowed = Array.isArray(binding.viewArgs) ? binding.viewArgs : [];
242
- var suppliedKeys = Object.keys(supplied);
243
- for (var i = 0; i < suppliedKeys.length; i++) {
244
- var key = suppliedKeys[i];
245
- if (allowed.indexOf(key) === -1) {
246
- finishInnerRead(message.id, false, "Undeclared read argument " + JSON.stringify(key));
247
- return;
248
- }
249
- }
250
- var args = Object.create(null);
251
- var fixed = binding.fixedArgs && typeof binding.fixedArgs === "object"
252
- ? binding.fixedArgs
253
- : {};
254
- Object.keys(fixed).forEach(function (key) { args[key] = fixed[key]; });
255
- suppliedKeys.forEach(function (key) { args[key] = supplied[key]; });
256
-
257
- var hostId = "connecta-ui-read-" + String(++nextHostRequestId);
258
- pendingHostReads[hostId] = { innerId: message.id };
259
- activeHostReads++;
260
- send({
261
- jsonrpc: "2.0",
262
- id: hostId,
263
- method: "tools/call",
264
- params: {
265
- name: "call_tool",
266
- arguments: {
267
- address: binding.address,
268
- args: args,
269
- resultMode: "value"
270
- }
271
- }
272
- });
273
- }
274
-
275
- function finishHostRead(message) {
276
- var pending = pendingHostReads[message.id];
277
- if (!pending) return false;
278
- delete pendingHostReads[message.id];
279
- activeHostReads--;
280
- if (message.error) {
281
- finishInnerRead(pending.innerId, false, readError(message.error, "Host rejected read"));
282
- return true;
283
- }
284
- var toolResult = message.result;
285
- if (!toolResult || typeof toolResult !== "object") {
286
- finishInnerRead(pending.innerId, false, "Host returned an invalid tool result");
287
- return true;
288
- }
289
- var structured = toolResult.structuredContent;
290
- if (toolResult.isError || (structured && structured.ok === false)) {
291
- var detail = structured && structured.error;
292
- var content = Array.isArray(toolResult.content)
293
- ? toolResult.content.find(function (block) { return block && block.type === "text"; })
294
- : null;
295
- finishInnerRead(
296
- pending.innerId,
297
- false,
298
- readError(detail, content && content.text ? content.text : "Read failed")
299
- );
300
- return true;
301
- }
302
- var value = structured && structured.ok === true &&
303
- Object.prototype.hasOwnProperty.call(structured, "data")
304
- ? structured.data
305
- : structured !== undefined
306
- ? structured
307
- : toolResult;
308
- finishInnerRead(pending.innerId, true, value);
309
- return true;
310
- }
311
-
312
128
  window.addEventListener("message", function (event) {
313
- var message = event.data;
314
- if (event.source === view.contentWindow) {
315
- if (message && message.type === "connecta/read") beginInnerRead(message);
316
- return;
317
- }
318
129
  if (event.source !== host) return;
130
+ var message = event.data;
319
131
  if (!message || message.jsonrpc !== "2.0") return;
320
- if (message.id !== undefined && finishHostRead(message)) return;
321
132
  if (message.method === "ui/notifications/tool-result") {
322
133
  render(message.params);
323
134
  return;
@@ -335,8 +146,6 @@ export const PROGRAM_UI_SHELL_HTML = `<!doctype html>
335
146
  // response carries the same id, and announcing initialization on one
336
147
  // would assert a handshake that never happened.
337
148
  if (message.id === initializeId && message.result !== undefined) {
338
- var capabilities = message.result.hostCapabilities;
339
- hostCanCallTools = Boolean(capabilities && capabilities.serverTools);
340
149
  notify("ui/notifications/initialized", {});
341
150
  }
342
151
  });
@@ -5,7 +5,7 @@ export interface BearerTokenOptions {
5
5
  }
6
6
  /**
7
7
  * Static bearer-token inbound auth. Constant-time compares the Bearer token
8
- * against `secret`. Checked BEFORE the Clerk gate in the server; a mismatch
9
- * falls through so a co-configured Clerk provider can still admit the request.
8
+ * against `secret`. Checked before interactive providers in the server; a
9
+ * mismatch falls through so another configured provider can admit the request.
10
10
  */
11
11
  export declare function bearerToken(secret: string, options?: BearerTokenOptions): InboundAuth;
@@ -16,8 +16,8 @@ function timingSafeEqual(a, b) {
16
16
  }
17
17
  /**
18
18
  * Static bearer-token inbound auth. Constant-time compares the Bearer token
19
- * against `secret`. Checked BEFORE the Clerk gate in the server; a mismatch
20
- * falls through so a co-configured Clerk provider can still admit the request.
19
+ * against `secret`. Checked before interactive providers in the server; a
20
+ * mismatch falls through so another configured provider can admit the request.
21
21
  */
22
22
  export function bearerToken(secret, options = {}) {
23
23
  assertNoRetiredToolkitOptions("bearerToken", options);
@@ -366,6 +366,7 @@ export function clerkAuth(opts) {
366
366
  };
367
367
  return {
368
368
  kind: "clerk",
369
+ interactiveOperator: true,
369
370
  activityActorNamespace: frontendApiUrl,
370
371
  activityActorLabel: resolveActivityLabel,
371
372
  uiAuth: {
@@ -0,0 +1,8 @@
1
+ import type { InboundAuth } from "../types.js";
2
+ /**
3
+ * Trust the identity Cloudflare Access attached to this direct Worker
4
+ * invocation. Access has already validated the browser session, Managed OAuth
5
+ * token, or service-token headers before the Worker runs; this adapter does
6
+ * not accept or parse a caller-supplied JWT.
7
+ */
8
+ export declare function cloudflareAccessAuth(): InboundAuth;