@agent-native/core 0.22.27 → 0.22.29

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.
@@ -1 +1 @@
1
- {"version":3,"file":"open-route.js","sourceRoot":"","sources":["../../src/server/open-route.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `/_agent-native/open` — the stable deep-link route.\n *\n * An external coding agent (Claude Code / Cowork / Codex) surfaces an\n * \"Open in <app> →\" link (built by an action's `link` builder, see\n * `deep-link.ts`). When the user clicks it in any browser / inline webview,\n * this route:\n * 1. Resolves the *browser* session (NOT the agent token) — so the record\n * always lands where the human is logged in.\n * 2. When unauthenticated, serves the same sign-in form the auth guard\n * would, *at this same URL*. The login form's success handler reloads\n * `window.location.href`, so the now-authenticated request re-enters\n * this route and proceeds. No `?next=` plumbing needed.\n * 3. Writes the existing one-shot `navigate` application-state command (the\n * exact key the UI already drains every 2s — we don't invent a new\n * navigation mechanism, we bridge to it), plus an optional `compose-<id>`\n * draft.\n * 4. 302-redirects to the rendered SPA view so the page loads immediately;\n * the polled `navigate` command then applies record-level focus.\n *\n * The link itself is a pure pointer (view + record ids + filters) and carries\n * no privileged state.\n */\nimport type { H3Event } from \"h3\";\nimport { defineEventHandler, getMethod } from \"h3\";\nimport { getSession, getConfiguredLoginHtml } from \"./auth.js\";\nimport { appStatePut, appStateGet } from \"../application-state/store.js\";\nimport {\n AGENT_SIDEBAR_QUERY_PARAM,\n withCollapsedAgentSidebarParam,\n} from \"../shared/agent-sidebar-url.js\";\nimport {\n EMBED_MODE_QUERY_PARAM,\n EMBED_TOKEN_QUERY_PARAM,\n MCP_APP_CHAT_BRIDGE_QUERY_PARAM,\n} from \"../shared/embed-auth.js\";\nimport { getConfiguredAppBasePath } from \"./app-base-path.js\";\n\n/** Query keys that are route control, not navigation payload. */\nconst RESERVED = new Set([\n \"app\",\n \"view\",\n \"to\",\n \"compose\",\n EMBED_MODE_QUERY_PARAM,\n EMBED_TOKEN_QUERY_PARAM,\n MCP_APP_CHAT_BRIDGE_QUERY_PARAM,\n AGENT_SIDEBAR_QUERY_PARAM,\n]);\n\n// Control-char guard (NUL..US + DEL). Defined via codepoints so the source\n// file stays plain ASCII.\nconst CONTROL_CHARS = new RegExp(\"[\\\\u0000-\\\\u001f\\\\u007f]\");\n\n// Compose-draft id charset. Mirrors `sanitizeDraftId` in\n// templates/mail/actions/manage-draft.ts so the id we concatenate into the\n// `compose-<id>` application-state key can't escape the key namespace\n// (path-traversal / key injection guard).\nconst COMPOSE_ID = /^[a-zA-Z0-9_-]{1,64}$/;\n\nexport interface OpenRouteOptions {\n /** Per-template override that turns the parsed deep-link params into the\n * client-side SPA path to redirect to. Return `null` to use the default\n * (`/<view>`). Filter params (`f_*`) are appended automatically. */\n resolveOpenPath?: (params: {\n app?: string;\n view?: string;\n params: Record<string, string>;\n }) => string | null | undefined;\n}\n\nfunction getRequestUrl(event: H3Event): string {\n const mountedPathname = (event as any).context?._mountedPathname;\n if (typeof mountedPathname === \"string\" && mountedPathname) {\n return `${mountedPathname}${(event as any).url?.search ?? \"\"}`;\n }\n return (event as any).node?.req?.url ?? (event as any).path ?? \"/\";\n}\n\n/** Decode a base64url string to UTF-8 (Node Buffer; this route is Node-only). */\nfunction decodeBase64Url(input: string): string {\n return Buffer.from(input, \"base64url\").toString(\"utf8\");\n}\n\n/**\n * Normalize a candidate redirect path to a safe, same-origin, leading-slash\n * relative path. Rejects absolute URLs, scheme-relative `//host`, and control\n * chars (open-redirect guard). Returns `null` when unsafe.\n */\nfunction safeRelativePath(raw: string | undefined | null): string | null {\n if (!raw) return null;\n if (CONTROL_CHARS.test(raw)) return null;\n if (!raw.startsWith(\"/\")) return null;\n if (raw.startsWith(\"//\") || raw.startsWith(\"/\\\\\")) return null;\n if (/^\\/[a-z][a-z0-9+.-]*:/i.test(raw)) return null;\n return raw;\n}\n\nfunction redirect(location: string): Response {\n // Native web Response (not h3 v2's reworked sendRedirect) — matches the\n // redirect pattern used elsewhere in auth.ts.\n return new Response(\"\", { status: 302, headers: { Location: location } });\n}\n\nfunction appendSearchParams(target: string, params: URLSearchParams): string {\n if (!params.toString()) return target;\n try {\n const url = new URL(target, \"http://an.invalid\");\n for (const [k, v] of params.entries()) url.searchParams.set(k, v);\n return `${url.pathname}${url.search}${url.hash}`;\n } catch {\n return target;\n }\n}\n\nfunction withConfiguredRedirectBasePath(target: string): string {\n const base = getConfiguredAppBasePath();\n if (!base) return target;\n try {\n const url = new URL(target, \"http://an.invalid\");\n if (url.pathname === base || url.pathname.startsWith(`${base}/`)) {\n return `${url.pathname}${url.search}${url.hash}`;\n }\n url.pathname = url.pathname === \"/\" ? base : `${base}${url.pathname}`;\n return `${url.pathname}${url.search}${url.hash}`;\n } catch {\n return target;\n }\n}\n\nexport function createOpenRouteHandler(options: OpenRouteOptions = {}) {\n return defineEventHandler(async (event: H3Event) => {\n const method = getMethod(event);\n if (method !== \"GET\" && method !== \"HEAD\") {\n return new Response(JSON.stringify({ error: \"Method not allowed\" }), {\n status: 405,\n headers: { \"Content-Type\": \"application/json\" },\n });\n }\n\n const rawUrl = getRequestUrl(event);\n let search: URLSearchParams;\n try {\n search = new URL(rawUrl, \"http://an.invalid\").searchParams;\n } catch {\n search = new URLSearchParams();\n }\n\n const app = search.get(\"app\") ?? undefined;\n const view = search.get(\"view\") ?? undefined;\n const toParam = search.get(\"to\") ?? undefined;\n const compose = search.get(\"compose\") ?? undefined;\n\n // Resolve the BROWSER session. When unauthenticated, serve the same login\n // form the guard would — at this URL — so the post-login reload returns\n // here authenticated.\n const session = await getSession(event);\n if (!session?.email) {\n const html = getConfiguredLoginHtml(event);\n if (html) {\n return new Response(html, {\n status: 200,\n headers: { \"Content-Type\": \"text/html; charset=utf-8\" },\n });\n }\n // No auth guard configured (fully open app) — best effort: still send\n // the user to the view; nothing to scope the navigate write to.\n }\n\n // Build the navigation payload from every non-reserved query param\n // (record ids + filters: threadId, eventId, dashboardId, f_*, ...).\n const navParams: Record<string, string> = {};\n for (const [k, v] of search.entries()) {\n if (RESERVED.has(k)) continue;\n navParams[k] = v;\n }\n const navPayload: Record<string, unknown> = { ...navParams };\n if (view) navPayload.view = view;\n\n if (session?.email) {\n try {\n await appStatePut(session.email, \"navigate\", navPayload, {\n requestSource: \"deep-link\",\n });\n if (compose) {\n try {\n const draft = JSON.parse(decodeBase64Url(compose));\n // Validate the id before using it as a key segment. An unsafe id\n // could escape the `compose-` namespace and clobber an unrelated\n // application-state key; skip the write (the view still opens),\n // mirroring the malformed-payload branch below.\n if (\n draft &&\n typeof draft === \"object\" &&\n typeof draft.id === \"string\" &&\n COMPOSE_ID.test(draft.id)\n ) {\n const composeKey = `compose-${draft.id}`;\n // A compact deep link may carry only `{ id, subject }` when the\n // full draft was too large to inline in the URL. The complete\n // draft is already persisted at `compose-<id>` by manage-draft\n // on create/update. Never let the truncated stub overwrite that\n // richer saved draft (would silently lose body / recipients /\n // reply metadata). Only write when the payload actually carries\n // content, or when nothing is saved yet (composer still opens).\n const hasContent =\n (typeof draft.body === \"string\" && draft.body.length > 0) ||\n !!draft.to ||\n !!draft.cc ||\n !!draft.bcc ||\n !!draft.html ||\n !!draft.replyToThreadId;\n const existing = hasContent\n ? null\n : await appStateGet(session.email, composeKey);\n if (hasContent || !existing) {\n await appStatePut(session.email, composeKey, draft, {\n requestSource: \"deep-link\",\n });\n }\n }\n } catch {\n // Malformed compose payload — skip; the view still opens.\n }\n }\n } catch {\n // App-state write failure shouldn't 500 the click; the redirect\n // below still lands the user on the right view.\n }\n }\n\n // Resolve the SPA path to redirect to.\n let target =\n safeRelativePath(toParam) ??\n safeRelativePath(\n options.resolveOpenPath?.({ app, view, params: navParams }) ??\n (view ? `/${view}` : null),\n ) ??\n \"/\";\n\n // Forward filter params (f_*) onto the redirect so dashboards/lists open\n // pre-filtered even before the navigate command is drained.\n const filters = new URLSearchParams();\n for (const [k, v] of search.entries()) {\n if (k.startsWith(\"f_\")) filters.set(k, v);\n }\n target = appendSearchParams(target, filters);\n const embedParams = new URLSearchParams();\n for (const key of [\n EMBED_MODE_QUERY_PARAM,\n EMBED_TOKEN_QUERY_PARAM,\n MCP_APP_CHAT_BRIDGE_QUERY_PARAM,\n ]) {\n const value = search.get(key);\n if (value) embedParams.set(key, value);\n }\n target = appendSearchParams(target, embedParams);\n target = withCollapsedAgentSidebarParam(target);\n target = withConfiguredRedirectBasePath(target);\n\n return redirect(target);\n });\n}\n"]}
1
+ {"version":3,"file":"open-route.js","sourceRoot":"","sources":["../../src/server/open-route.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `/_agent-native/open` — the stable deep-link route.\n *\n * An external coding agent (Claude Code / Cowork / Codex) surfaces an\n * \"Open in <app> →\" link (built by an action's `link` builder, see\n * `deep-link.ts`). When the user clicks it in any browser / inline webview,\n * this route:\n * 1. Resolves the *browser* session (NOT the agent token) — so the record\n * always lands where the human is logged in.\n * 2. When unauthenticated, serves the same sign-in form the auth guard\n * would, *at this same URL*. The login form's success handler reloads\n * `window.location.href`, so the now-authenticated request re-enters\n * this route and proceeds. No `?next=` plumbing needed.\n * 3. Writes the existing one-shot `navigate` application-state command (the\n * exact key the UI already drains every 2s — we don't invent a new\n * navigation mechanism, we bridge to it), plus an optional `compose-<id>`\n * draft.\n * 4. 302-redirects to the rendered SPA view so the page loads immediately;\n * the polled `navigate` command then applies record-level focus.\n *\n * The link itself is a pure pointer (view + record ids + filters) and carries\n * no privileged state.\n */\nimport type { H3Event } from \"h3\";\nimport { defineEventHandler, getHeader, getMethod } from \"h3\";\nimport { getSession, getConfiguredLoginHtml } from \"./auth.js\";\nimport { appStatePut, appStateGet } from \"../application-state/store.js\";\nimport { requestHasEmbedAuthMarker } from \"./embed-session.js\";\nimport {\n AGENT_SIDEBAR_QUERY_PARAM,\n withCollapsedAgentSidebarParam,\n} from \"../shared/agent-sidebar-url.js\";\nimport {\n EMBED_MODE_QUERY_PARAM,\n EMBED_TOKEN_QUERY_PARAM,\n MCP_APP_CHAT_BRIDGE_QUERY_PARAM,\n} from \"../shared/embed-auth.js\";\nimport { getConfiguredAppBasePath } from \"./app-base-path.js\";\nimport {\n isMcpEmbedCorsOrigin,\n MCP_EMBED_CORS_ALLOW_HEADERS,\n} from \"../shared/mcp-embed-headers.js\";\n\n/** Query keys that are route control, not navigation payload. */\nconst RESERVED = new Set([\n \"app\",\n \"view\",\n \"to\",\n \"compose\",\n EMBED_MODE_QUERY_PARAM,\n EMBED_TOKEN_QUERY_PARAM,\n MCP_APP_CHAT_BRIDGE_QUERY_PARAM,\n AGENT_SIDEBAR_QUERY_PARAM,\n]);\n\n// Control-char guard (NUL..US + DEL). Defined via codepoints so the source\n// file stays plain ASCII.\nconst CONTROL_CHARS = new RegExp(\"[\\\\u0000-\\\\u001f\\\\u007f]\");\n\n// Compose-draft id charset. Mirrors `sanitizeDraftId` in\n// templates/mail/actions/manage-draft.ts so the id we concatenate into the\n// `compose-<id>` application-state key can't escape the key namespace\n// (path-traversal / key injection guard).\nconst COMPOSE_ID = /^[a-zA-Z0-9_-]{1,64}$/;\n\nexport interface OpenRouteOptions {\n /** Per-template override that turns the parsed deep-link params into the\n * client-side SPA path to redirect to. Return `null` to use the default\n * (`/<view>`). Filter params (`f_*`) are appended automatically. */\n resolveOpenPath?: (params: {\n app?: string;\n view?: string;\n params: Record<string, string>;\n }) => string | null | undefined;\n}\n\nfunction getRequestUrl(event: H3Event): string {\n const mountedPathname = (event as any).context?._mountedPathname;\n if (typeof mountedPathname === \"string\" && mountedPathname) {\n return `${mountedPathname}${(event as any).url?.search ?? \"\"}`;\n }\n return (event as any).node?.req?.url ?? (event as any).path ?? \"/\";\n}\n\n/** Decode a base64url string to UTF-8 (Node Buffer; this route is Node-only). */\nfunction decodeBase64Url(input: string): string {\n return Buffer.from(input, \"base64url\").toString(\"utf8\");\n}\n\n/**\n * Normalize a candidate redirect path to a safe, same-origin, leading-slash\n * relative path. Rejects absolute URLs, scheme-relative `//host`, and control\n * chars (open-redirect guard). Returns `null` when unsafe.\n */\nfunction safeRelativePath(raw: string | undefined | null): string | null {\n if (!raw) return null;\n if (CONTROL_CHARS.test(raw)) return null;\n if (!raw.startsWith(\"/\")) return null;\n if (raw.startsWith(\"//\") || raw.startsWith(\"/\\\\\")) return null;\n if (/^\\/[a-z][a-z0-9+.-]*:/i.test(raw)) return null;\n return raw;\n}\n\nfunction addMcpEmbedHeaders(event: H3Event, headers: Headers): Headers {\n headers.set(\"Cross-Origin-Embedder-Policy\", \"require-corp\");\n headers.set(\"Cross-Origin-Opener-Policy\", \"same-origin\");\n headers.set(\"Cross-Origin-Resource-Policy\", \"cross-origin\");\n headers.set(\"Referrer-Policy\", \"no-referrer\");\n const origin = getHeader(event, \"origin\");\n if (isMcpEmbedCorsOrigin(origin)) {\n headers.set(\"Access-Control-Allow-Origin\", origin);\n headers.set(\"Vary\", \"Origin\");\n headers.set(\"Access-Control-Allow-Methods\", \"GET,HEAD,OPTIONS\");\n headers.set(\"Access-Control-Allow-Headers\", MCP_EMBED_CORS_ALLOW_HEADERS);\n headers.set(\"Access-Control-Expose-Headers\", \"Location\");\n }\n return headers;\n}\n\nfunction redirect(\n event: H3Event,\n location: string,\n embedRedirect: boolean,\n): Response {\n // Native web Response (not h3 v2's reworked sendRedirect) — matches the\n // redirect pattern used elsewhere in auth.ts.\n const headers = new Headers({ Location: location });\n if (embedRedirect) addMcpEmbedHeaders(event, headers);\n return new Response(\"\", { status: 302, headers });\n}\n\nfunction appendSearchParams(target: string, params: URLSearchParams): string {\n if (!params.toString()) return target;\n try {\n const url = new URL(target, \"http://an.invalid\");\n for (const [k, v] of params.entries()) url.searchParams.set(k, v);\n return `${url.pathname}${url.search}${url.hash}`;\n } catch {\n return target;\n }\n}\n\nfunction withConfiguredRedirectBasePath(target: string): string {\n const base = getConfiguredAppBasePath();\n if (!base) return target;\n try {\n const url = new URL(target, \"http://an.invalid\");\n if (url.pathname === base || url.pathname.startsWith(`${base}/`)) {\n return `${url.pathname}${url.search}${url.hash}`;\n }\n url.pathname = url.pathname === \"/\" ? base : `${base}${url.pathname}`;\n return `${url.pathname}${url.search}${url.hash}`;\n } catch {\n return target;\n }\n}\n\nexport function createOpenRouteHandler(options: OpenRouteOptions = {}) {\n return defineEventHandler(async (event: H3Event) => {\n const method = getMethod(event);\n if (method !== \"GET\" && method !== \"HEAD\") {\n return new Response(JSON.stringify({ error: \"Method not allowed\" }), {\n status: 405,\n headers: { \"Content-Type\": \"application/json\" },\n });\n }\n\n const rawUrl = getRequestUrl(event);\n let search: URLSearchParams;\n try {\n search = new URL(rawUrl, \"http://an.invalid\").searchParams;\n } catch {\n search = new URLSearchParams();\n }\n\n const app = search.get(\"app\") ?? undefined;\n const view = search.get(\"view\") ?? undefined;\n const toParam = search.get(\"to\") ?? undefined;\n const compose = search.get(\"compose\") ?? undefined;\n\n // Resolve the BROWSER session. When unauthenticated, serve the same login\n // form the guard would — at this URL — so the post-login reload returns\n // here authenticated.\n const session = await getSession(event);\n if (!session?.email) {\n const html = getConfiguredLoginHtml(event);\n if (html) {\n return new Response(html, {\n status: 200,\n headers: { \"Content-Type\": \"text/html; charset=utf-8\" },\n });\n }\n // No auth guard configured (fully open app) — best effort: still send\n // the user to the view; nothing to scope the navigate write to.\n }\n\n // Build the navigation payload from every non-reserved query param\n // (record ids + filters: threadId, eventId, dashboardId, f_*, ...).\n const navParams: Record<string, string> = {};\n for (const [k, v] of search.entries()) {\n if (RESERVED.has(k)) continue;\n navParams[k] = v;\n }\n const navPayload: Record<string, unknown> = { ...navParams };\n if (view) navPayload.view = view;\n\n if (session?.email) {\n try {\n await appStatePut(session.email, \"navigate\", navPayload, {\n requestSource: \"deep-link\",\n });\n if (compose) {\n try {\n const draft = JSON.parse(decodeBase64Url(compose));\n // Validate the id before using it as a key segment. An unsafe id\n // could escape the `compose-` namespace and clobber an unrelated\n // application-state key; skip the write (the view still opens),\n // mirroring the malformed-payload branch below.\n if (\n draft &&\n typeof draft === \"object\" &&\n typeof draft.id === \"string\" &&\n COMPOSE_ID.test(draft.id)\n ) {\n const composeKey = `compose-${draft.id}`;\n // A compact deep link may carry only `{ id, subject }` when the\n // full draft was too large to inline in the URL. The complete\n // draft is already persisted at `compose-<id>` by manage-draft\n // on create/update. Never let the truncated stub overwrite that\n // richer saved draft (would silently lose body / recipients /\n // reply metadata). Only write when the payload actually carries\n // content, or when nothing is saved yet (composer still opens).\n const hasContent =\n (typeof draft.body === \"string\" && draft.body.length > 0) ||\n !!draft.to ||\n !!draft.cc ||\n !!draft.bcc ||\n !!draft.html ||\n !!draft.replyToThreadId;\n const existing = hasContent\n ? null\n : await appStateGet(session.email, composeKey);\n if (hasContent || !existing) {\n await appStatePut(session.email, composeKey, draft, {\n requestSource: \"deep-link\",\n });\n }\n }\n } catch {\n // Malformed compose payload — skip; the view still opens.\n }\n }\n } catch {\n // App-state write failure shouldn't 500 the click; the redirect\n // below still lands the user on the right view.\n }\n }\n\n // Resolve the SPA path to redirect to.\n let target =\n safeRelativePath(toParam) ??\n safeRelativePath(\n options.resolveOpenPath?.({ app, view, params: navParams }) ??\n (view ? `/${view}` : null),\n ) ??\n \"/\";\n\n // Forward filter params (f_*) onto the redirect so dashboards/lists open\n // pre-filtered even before the navigate command is drained.\n const filters = new URLSearchParams();\n for (const [k, v] of search.entries()) {\n if (k.startsWith(\"f_\")) filters.set(k, v);\n }\n target = appendSearchParams(target, filters);\n const embedParams = new URLSearchParams();\n for (const key of [\n EMBED_MODE_QUERY_PARAM,\n EMBED_TOKEN_QUERY_PARAM,\n MCP_APP_CHAT_BRIDGE_QUERY_PARAM,\n ]) {\n const value = search.get(key);\n if (value) embedParams.set(key, value);\n }\n target = appendSearchParams(target, embedParams);\n target = withCollapsedAgentSidebarParam(target);\n target = withConfiguredRedirectBasePath(target);\n\n return redirect(event, target, requestHasEmbedAuthMarker(event));\n });\n}\n"]}
@@ -51,7 +51,7 @@
51
51
  */
52
52
  import { defineEventHandler, getHeader, setResponseHeader } from "h3";
53
53
  import { requestHasEmbedAuthMarker } from "./embed-session.js";
54
- import { isClaudeMcpContentOrigin, MCP_EMBED_CORS_ALLOW_HEADERS, } from "../shared/mcp-embed-headers.js";
54
+ import { isMcpEmbedCorsOrigin, MCP_EMBED_CORS_ALLOW_HEADERS, } from "../shared/mcp-embed-headers.js";
55
55
  const HSTS = "max-age=31536000; includeSubDomains; preload";
56
56
  const PERMISSIONS_POLICY = "camera=(), microphone=(self), geolocation=(), screen-wake-lock=()";
57
57
  /**
@@ -99,7 +99,7 @@ export function createSecurityHeadersMiddleware() {
99
99
  setResponseHeader(event, "Cross-Origin-Embedder-Policy", "require-corp");
100
100
  }
101
101
  setResponseHeader(event, "Cross-Origin-Resource-Policy", embedFrameRequest ? "cross-origin" : "same-site");
102
- if (embedFrameRequest && isClaudeMcpContentOrigin(requestOrigin)) {
102
+ if (embedFrameRequest && isMcpEmbedCorsOrigin(requestOrigin)) {
103
103
  setResponseHeader(event, "Access-Control-Allow-Origin", requestOrigin);
104
104
  setResponseHeader(event, "Vary", "Origin");
105
105
  setResponseHeader(event, "Access-Control-Allow-Methods", "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS");
@@ -1 +1 @@
1
- {"version":3,"file":"security-headers.js","sourceRoot":"","sources":["../../src/server/security-headers.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Security response headers middleware.\n *\n * Sets a baseline set of \"no-brainer\" security headers on every framework HTTP\n * response. These headers are layered defenses: each one mitigates a specific\n * class of attack, and together they harden the surface against clickjacking,\n * MIME-sniffing, referrer leakage, mixed-content downgrades, and cross-origin\n * window/embed access.\n *\n * The headers we emit:\n *\n * - `Strict-Transport-Security` — forces HTTPS for the browser's lifetime\n * of the cached value, preventing SSL-strip MITM. Only emitted when the\n * request scheme is `https` (we don't want to break local-dev HTTP, and\n * emitting HSTS over HTTP is a no-op per the spec but causes confusion).\n * - `X-Content-Type-Options: nosniff` — disables browser MIME sniffing so\n * a tool /render route serving user-authored HTML can't be misinterpreted\n * as some other content type by a clever Accept header.\n * - `X-Frame-Options: DENY` — prevents the entire app from being iframed by\n * other origins (clickjacking the agent chat, booking pages, etc.). The\n * tool /render endpoint and any other route that legitimately needs to be\n * embedded in the same-origin app shell can opt out by setting its own\n * header inside the route handler — h3's `setResponseHeader` overwrites,\n * so a route emitting `SAMEORIGIN` wins over our middleware default.\n * We skip this header entirely in dev (NODE_ENV !== \"production\") so the\n * desktop app's local dev frame (localhost:3334) can iframe templates\n * running on other localhost ports (e.g. mail at 8085).\n * - `Referrer-Policy: strict-origin-when-cross-origin` — strips path/query\n * from outbound Referer headers when the request crosses origin, so a\n * public-share viewer's outbound link clicks never leak the share token.\n * - `Permissions-Policy: camera=(), microphone=(self), geolocation=(),\n * screen-wake-lock=()` — allows the app shell to request microphone access\n * for composer dictation while keeping camera/location/wake-lock blocked\n * by default. Templates that need broader media capture for recording UI\n * override this on their own routes.\n * - `Cross-Origin-Opener-Policy: same-origin` — isolates window.opener so\n * a popup-window opener reference can't read or modify our document.\n * - `Cross-Origin-Embedder-Policy: require-corp` — emitted only for\n * validated MCP embed-session page loads. COEP hosts such as Claude's MCP\n * Apps proxy require framed cross-origin documents to opt in explicitly.\n * - `Cross-Origin-Resource-Policy: same-site` — prevents other origins from\n * embedding our endpoints as `<img>` / `<script>` / `<audio>`, blocking\n * the simplest data-leak chain when combined with auth cookies. Validated\n * MCP embed-session page loads use `cross-origin` so COEP hosts such as\n * Claude's MCP Apps proxy can frame the short-lived app document.\n *\n * NOTE: We don't set `Cross-Origin-Embedder-Policy` because it requires every\n * embedded subresource to opt in via CORP/CORS, which would break Builder's\n * iframe editor and template embed use cases. COOP + CORP without COEP gives\n * us most of the protection.\n */\n\nimport { defineEventHandler, getHeader, setResponseHeader } from \"h3\";\nimport { requestHasEmbedAuthMarker } from \"./embed-session.js\";\nimport {\n isClaudeMcpContentOrigin,\n MCP_EMBED_CORS_ALLOW_HEADERS,\n} from \"../shared/mcp-embed-headers.js\";\n\nconst HSTS = \"max-age=31536000; includeSubDomains; preload\";\nconst PERMISSIONS_POLICY =\n \"camera=(), microphone=(self), geolocation=(), screen-wake-lock=()\";\n\n/**\n * Returns true when the request was received over HTTPS. We trust both the\n * underlying connection (when the server is terminating TLS itself) and the\n * `x-forwarded-proto` header (set by Netlify, Vercel, Cloudflare, and any\n * other reverse proxy that fronts the framework).\n */\nfunction isHttpsRequest(event: any): boolean {\n const xfp =\n event?.node?.req?.headers?.[\"x-forwarded-proto\"] ??\n event?.headers?.get?.(\"x-forwarded-proto\");\n if (typeof xfp === \"string\" && xfp.split(\",\")[0].trim() === \"https\")\n return true;\n if (Array.isArray(xfp) && xfp[0] === \"https\") return true;\n // h3 sets `event.url.protocol` to \"http:\" or \"https:\".\n const proto = event?.url?.protocol;\n if (proto === \"https:\") return true;\n // Direct Node `req.connection.encrypted` (older runtimes).\n if (event?.node?.req?.connection?.encrypted) return true;\n return false;\n}\n\n/**\n * Create the security-headers h3 middleware. Mount this BEFORE other route\n * handlers so the headers are present on every response (including 4xx/5xx\n * error pages). Route handlers that need to relax a specific header (e.g.\n * `X-Frame-Options: SAMEORIGIN` on the tool render route) can call\n * `setResponseHeader` after this runs — the latest write wins.\n */\nexport function createSecurityHeadersMiddleware() {\n const isProduction = process.env.NODE_ENV === \"production\";\n return defineEventHandler((event) => {\n const embedFrameRequest = requestHasEmbedAuthMarker(event);\n const requestOrigin = getHeader(event, \"origin\");\n setResponseHeader(event, \"X-Content-Type-Options\", \"nosniff\");\n if (isProduction && !embedFrameRequest) {\n setResponseHeader(event, \"X-Frame-Options\", \"DENY\");\n }\n setResponseHeader(\n event,\n \"Referrer-Policy\",\n embedFrameRequest ? \"no-referrer\" : \"strict-origin-when-cross-origin\",\n );\n setResponseHeader(event, \"Permissions-Policy\", PERMISSIONS_POLICY);\n setResponseHeader(event, \"Cross-Origin-Opener-Policy\", \"same-origin\");\n if (embedFrameRequest) {\n setResponseHeader(event, \"Cross-Origin-Embedder-Policy\", \"require-corp\");\n }\n setResponseHeader(\n event,\n \"Cross-Origin-Resource-Policy\",\n embedFrameRequest ? \"cross-origin\" : \"same-site\",\n );\n if (embedFrameRequest && isClaudeMcpContentOrigin(requestOrigin)) {\n setResponseHeader(event, \"Access-Control-Allow-Origin\", requestOrigin);\n setResponseHeader(event, \"Vary\", \"Origin\");\n setResponseHeader(\n event,\n \"Access-Control-Allow-Methods\",\n \"GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS\",\n );\n setResponseHeader(\n event,\n \"Access-Control-Allow-Headers\",\n MCP_EMBED_CORS_ALLOW_HEADERS,\n );\n }\n if (isHttpsRequest(event)) {\n setResponseHeader(event, \"Strict-Transport-Security\", HSTS);\n }\n // Continue to the next handler — we only set headers, don't return a body.\n return undefined;\n });\n}\n"]}
1
+ {"version":3,"file":"security-headers.js","sourceRoot":"","sources":["../../src/server/security-headers.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Security response headers middleware.\n *\n * Sets a baseline set of \"no-brainer\" security headers on every framework HTTP\n * response. These headers are layered defenses: each one mitigates a specific\n * class of attack, and together they harden the surface against clickjacking,\n * MIME-sniffing, referrer leakage, mixed-content downgrades, and cross-origin\n * window/embed access.\n *\n * The headers we emit:\n *\n * - `Strict-Transport-Security` — forces HTTPS for the browser's lifetime\n * of the cached value, preventing SSL-strip MITM. Only emitted when the\n * request scheme is `https` (we don't want to break local-dev HTTP, and\n * emitting HSTS over HTTP is a no-op per the spec but causes confusion).\n * - `X-Content-Type-Options: nosniff` — disables browser MIME sniffing so\n * a tool /render route serving user-authored HTML can't be misinterpreted\n * as some other content type by a clever Accept header.\n * - `X-Frame-Options: DENY` — prevents the entire app from being iframed by\n * other origins (clickjacking the agent chat, booking pages, etc.). The\n * tool /render endpoint and any other route that legitimately needs to be\n * embedded in the same-origin app shell can opt out by setting its own\n * header inside the route handler — h3's `setResponseHeader` overwrites,\n * so a route emitting `SAMEORIGIN` wins over our middleware default.\n * We skip this header entirely in dev (NODE_ENV !== \"production\") so the\n * desktop app's local dev frame (localhost:3334) can iframe templates\n * running on other localhost ports (e.g. mail at 8085).\n * - `Referrer-Policy: strict-origin-when-cross-origin` — strips path/query\n * from outbound Referer headers when the request crosses origin, so a\n * public-share viewer's outbound link clicks never leak the share token.\n * - `Permissions-Policy: camera=(), microphone=(self), geolocation=(),\n * screen-wake-lock=()` — allows the app shell to request microphone access\n * for composer dictation while keeping camera/location/wake-lock blocked\n * by default. Templates that need broader media capture for recording UI\n * override this on their own routes.\n * - `Cross-Origin-Opener-Policy: same-origin` — isolates window.opener so\n * a popup-window opener reference can't read or modify our document.\n * - `Cross-Origin-Embedder-Policy: require-corp` — emitted only for\n * validated MCP embed-session page loads. COEP hosts such as Claude's MCP\n * Apps proxy require framed cross-origin documents to opt in explicitly.\n * - `Cross-Origin-Resource-Policy: same-site` — prevents other origins from\n * embedding our endpoints as `<img>` / `<script>` / `<audio>`, blocking\n * the simplest data-leak chain when combined with auth cookies. Validated\n * MCP embed-session page loads use `cross-origin` so COEP hosts such as\n * Claude's MCP Apps proxy can frame the short-lived app document.\n *\n * NOTE: We don't set `Cross-Origin-Embedder-Policy` because it requires every\n * embedded subresource to opt in via CORP/CORS, which would break Builder's\n * iframe editor and template embed use cases. COOP + CORP without COEP gives\n * us most of the protection.\n */\n\nimport { defineEventHandler, getHeader, setResponseHeader } from \"h3\";\nimport { requestHasEmbedAuthMarker } from \"./embed-session.js\";\nimport {\n isMcpEmbedCorsOrigin,\n MCP_EMBED_CORS_ALLOW_HEADERS,\n} from \"../shared/mcp-embed-headers.js\";\n\nconst HSTS = \"max-age=31536000; includeSubDomains; preload\";\nconst PERMISSIONS_POLICY =\n \"camera=(), microphone=(self), geolocation=(), screen-wake-lock=()\";\n\n/**\n * Returns true when the request was received over HTTPS. We trust both the\n * underlying connection (when the server is terminating TLS itself) and the\n * `x-forwarded-proto` header (set by Netlify, Vercel, Cloudflare, and any\n * other reverse proxy that fronts the framework).\n */\nfunction isHttpsRequest(event: any): boolean {\n const xfp =\n event?.node?.req?.headers?.[\"x-forwarded-proto\"] ??\n event?.headers?.get?.(\"x-forwarded-proto\");\n if (typeof xfp === \"string\" && xfp.split(\",\")[0].trim() === \"https\")\n return true;\n if (Array.isArray(xfp) && xfp[0] === \"https\") return true;\n // h3 sets `event.url.protocol` to \"http:\" or \"https:\".\n const proto = event?.url?.protocol;\n if (proto === \"https:\") return true;\n // Direct Node `req.connection.encrypted` (older runtimes).\n if (event?.node?.req?.connection?.encrypted) return true;\n return false;\n}\n\n/**\n * Create the security-headers h3 middleware. Mount this BEFORE other route\n * handlers so the headers are present on every response (including 4xx/5xx\n * error pages). Route handlers that need to relax a specific header (e.g.\n * `X-Frame-Options: SAMEORIGIN` on the tool render route) can call\n * `setResponseHeader` after this runs — the latest write wins.\n */\nexport function createSecurityHeadersMiddleware() {\n const isProduction = process.env.NODE_ENV === \"production\";\n return defineEventHandler((event) => {\n const embedFrameRequest = requestHasEmbedAuthMarker(event);\n const requestOrigin = getHeader(event, \"origin\");\n setResponseHeader(event, \"X-Content-Type-Options\", \"nosniff\");\n if (isProduction && !embedFrameRequest) {\n setResponseHeader(event, \"X-Frame-Options\", \"DENY\");\n }\n setResponseHeader(\n event,\n \"Referrer-Policy\",\n embedFrameRequest ? \"no-referrer\" : \"strict-origin-when-cross-origin\",\n );\n setResponseHeader(event, \"Permissions-Policy\", PERMISSIONS_POLICY);\n setResponseHeader(event, \"Cross-Origin-Opener-Policy\", \"same-origin\");\n if (embedFrameRequest) {\n setResponseHeader(event, \"Cross-Origin-Embedder-Policy\", \"require-corp\");\n }\n setResponseHeader(\n event,\n \"Cross-Origin-Resource-Policy\",\n embedFrameRequest ? \"cross-origin\" : \"same-site\",\n );\n if (embedFrameRequest && isMcpEmbedCorsOrigin(requestOrigin)) {\n setResponseHeader(event, \"Access-Control-Allow-Origin\", requestOrigin);\n setResponseHeader(event, \"Vary\", \"Origin\");\n setResponseHeader(\n event,\n \"Access-Control-Allow-Methods\",\n \"GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS\",\n );\n setResponseHeader(\n event,\n \"Access-Control-Allow-Headers\",\n MCP_EMBED_CORS_ALLOW_HEADERS,\n );\n }\n if (isHttpsRequest(event)) {\n setResponseHeader(event, \"Strict-Transport-Security\", HSTS);\n }\n // Continue to the next handler — we only set headers, don't return a body.\n return undefined;\n });\n}\n"]}
@@ -205,6 +205,11 @@ That makes the same app surface available to every compatible host rather than b
205
205
 
206
206
  Claude Code and other CLI-first clients still receive the same resources and metadata when they support MCP Apps, but the deep link remains the reliable fallback when a host chooses not to render an iframe. In practice, every agent-native app should be authored with both: MCP Apps for inline review/edit in capable hosts, and `link` for universal round-tripping back to the full app.
207
207
 
208
+ Claude and ChatGPT can cache tool and resource metadata for an existing custom
209
+ connector. After changing MCP App metadata, verify with a fresh tool call; if
210
+ the host still uses the old descriptor, reconnect the Claude connector or
211
+ rescan/review the ChatGPT connector so it refreshes the catalog.
212
+
208
213
  ### First-class MCP App bridge {#mcp-app-bridge}
209
214
 
210
215
  MCP App embeds are route embeds, not separate mini-products. `embedApp()`
@@ -388,8 +393,11 @@ Keep the existing `link` builder even when adding `mcpApp`. CLI-only clients, ol
388
393
 
389
394
  `embedApp()` includes the MCP request origin in the resource CSP so the launcher
390
395
  can fetch and, when explicitly requested, frame the signed first-party app
391
- route. Only pass additional `frameDomains` for a custom MCP App that truly
392
- embeds a third-party player.
396
+ route. Dispatch adds the exact origins for the granted apps to its `open_app`
397
+ resource so a single Dispatch connector can inline Mail, Calendar, Slides, and
398
+ the rest without allowing every HTTPS origin. Only pass additional frame or
399
+ resource domains for a custom MCP App that truly embeds a third-party player or
400
+ loads third-party assets.
393
401
 
394
402
  Inside those `embedApp()` routes, `sendToAgentChat()` is embed-aware.
395
403
  Auto-submitted prompts relay to the MCP host as `ui/update-model-context` plus
@@ -131,7 +131,10 @@ relay:
131
131
 
132
132
  `embedApp()` includes the MCP request origin in the resource CSP so the
133
133
  launcher can fetch and, when explicitly requested, frame the signed first-party
134
- route. Pass additional `frameDomains` only for custom third-party frames.
134
+ route. Dispatch's `open_app` resource adds the exact origins for apps granted
135
+ through Dispatch, which keeps the one-connector path narrow while still letting
136
+ Claude/ChatGPT inline target app routes. Pass additional domains only for
137
+ custom third-party frames or assets.
135
138
 
136
139
  Host-mediated open links keep the iframe from choosing its own browser target.
137
140
  Model context updates are opt-in and hidden from the user-facing transcript.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.22.27",
3
+ "version": "0.22.29",
4
4
  "type": "module",
5
5
  "description": "Framework for agent-native application development — where AI agents and UI share state via files",
6
6
  "license": "MIT",