@agent-native/core 0.19.1 → 0.20.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 (167) hide show
  1. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  2. package/dist/agent/engine/builder-engine.js +12 -1
  3. package/dist/agent/engine/builder-engine.js.map +1 -1
  4. package/dist/agent/production-agent.d.ts.map +1 -1
  5. package/dist/agent/production-agent.js +5 -0
  6. package/dist/agent/production-agent.js.map +1 -1
  7. package/dist/agent/thread-data-builder.d.ts +17 -0
  8. package/dist/agent/thread-data-builder.d.ts.map +1 -1
  9. package/dist/agent/thread-data-builder.js +210 -0
  10. package/dist/agent/thread-data-builder.js.map +1 -1
  11. package/dist/cli/code-agent-executor.d.ts +2 -0
  12. package/dist/cli/code-agent-executor.d.ts.map +1 -1
  13. package/dist/cli/code-agent-executor.js +39 -1
  14. package/dist/cli/code-agent-executor.js.map +1 -1
  15. package/dist/cli/code-agent-runs.d.ts +3 -0
  16. package/dist/cli/code-agent-runs.d.ts.map +1 -1
  17. package/dist/cli/code-agent-runs.js +3 -0
  18. package/dist/cli/code-agent-runs.js.map +1 -1
  19. package/dist/cli/connect.d.ts +3 -2
  20. package/dist/cli/connect.d.ts.map +1 -1
  21. package/dist/cli/connect.js +12 -8
  22. package/dist/cli/connect.js.map +1 -1
  23. package/dist/cli/mcp-config-writers.d.ts +3 -3
  24. package/dist/cli/mcp-config-writers.d.ts.map +1 -1
  25. package/dist/cli/mcp-config-writers.js +19 -8
  26. package/dist/cli/mcp-config-writers.js.map +1 -1
  27. package/dist/client/AgentPanel.d.ts.map +1 -1
  28. package/dist/client/AgentPanel.js +8 -3
  29. package/dist/client/AgentPanel.js.map +1 -1
  30. package/dist/client/AssistantChat.d.ts +58 -0
  31. package/dist/client/AssistantChat.d.ts.map +1 -1
  32. package/dist/client/AssistantChat.js +122 -50
  33. package/dist/client/AssistantChat.js.map +1 -1
  34. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  35. package/dist/client/agent-chat-adapter.js +172 -13
  36. package/dist/client/agent-chat-adapter.js.map +1 -1
  37. package/dist/client/agent-sidebar-state.d.ts +2 -0
  38. package/dist/client/agent-sidebar-state.d.ts.map +1 -1
  39. package/dist/client/agent-sidebar-state.js +32 -0
  40. package/dist/client/agent-sidebar-state.js.map +1 -1
  41. package/dist/client/code-agent-chat-adapter.d.ts +81 -0
  42. package/dist/client/code-agent-chat-adapter.d.ts.map +1 -0
  43. package/dist/client/code-agent-chat-adapter.js +297 -0
  44. package/dist/client/code-agent-chat-adapter.js.map +1 -0
  45. package/dist/client/composer/PromptComposer.d.ts +11 -0
  46. package/dist/client/composer/PromptComposer.d.ts.map +1 -1
  47. package/dist/client/composer/PromptComposer.js +7 -3
  48. package/dist/client/composer/PromptComposer.js.map +1 -1
  49. package/dist/client/composer/TiptapComposer.d.ts +12 -1
  50. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  51. package/dist/client/composer/TiptapComposer.js +16 -7
  52. package/dist/client/composer/TiptapComposer.js.map +1 -1
  53. package/dist/client/composer/index.d.ts +1 -0
  54. package/dist/client/composer/index.d.ts.map +1 -1
  55. package/dist/client/composer/index.js +1 -0
  56. package/dist/client/composer/index.js.map +1 -1
  57. package/dist/client/composer/prompt-attachments.d.ts +11 -0
  58. package/dist/client/composer/prompt-attachments.d.ts.map +1 -0
  59. package/dist/client/composer/prompt-attachments.js +45 -0
  60. package/dist/client/composer/prompt-attachments.js.map +1 -0
  61. package/dist/client/conversation/AgentConversation.d.ts.map +1 -1
  62. package/dist/client/conversation/AgentConversation.js +124 -1
  63. package/dist/client/conversation/AgentConversation.js.map +1 -1
  64. package/dist/client/conversation/code-agent-transcript.d.ts +25 -0
  65. package/dist/client/conversation/code-agent-transcript.d.ts.map +1 -0
  66. package/dist/client/conversation/code-agent-transcript.js +200 -0
  67. package/dist/client/conversation/code-agent-transcript.js.map +1 -0
  68. package/dist/client/conversation/index.d.ts +2 -1
  69. package/dist/client/conversation/index.d.ts.map +1 -1
  70. package/dist/client/conversation/index.js +1 -0
  71. package/dist/client/conversation/index.js.map +1 -1
  72. package/dist/client/conversation/types.d.ts +8 -0
  73. package/dist/client/conversation/types.d.ts.map +1 -1
  74. package/dist/client/conversation/types.js.map +1 -1
  75. package/dist/client/conversation/use-near-bottom-autoscroll.d.ts.map +1 -1
  76. package/dist/client/conversation/use-near-bottom-autoscroll.js +26 -9
  77. package/dist/client/conversation/use-near-bottom-autoscroll.js.map +1 -1
  78. package/dist/client/index.d.ts +5 -2
  79. package/dist/client/index.d.ts.map +1 -1
  80. package/dist/client/index.js +5 -2
  81. package/dist/client/index.js.map +1 -1
  82. package/dist/client/settings/useBuilderStatus.d.ts +2 -0
  83. package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
  84. package/dist/client/settings/useBuilderStatus.js +21 -3
  85. package/dist/client/settings/useBuilderStatus.js.map +1 -1
  86. package/dist/client/settings/useBuilderStatus.spec.js +16 -2
  87. package/dist/client/settings/useBuilderStatus.spec.js.map +1 -1
  88. package/dist/client/sse-event-processor.d.ts.map +1 -1
  89. package/dist/client/sse-event-processor.js +3 -0
  90. package/dist/client/sse-event-processor.js.map +1 -1
  91. package/dist/client/use-chat-models.d.ts +6 -1
  92. package/dist/client/use-chat-models.d.ts.map +1 -1
  93. package/dist/client/use-chat-models.js +7 -3
  94. package/dist/client/use-chat-models.js.map +1 -1
  95. package/dist/client/use-chat-models.spec.d.ts +2 -0
  96. package/dist/client/use-chat-models.spec.d.ts.map +1 -0
  97. package/dist/client/use-chat-models.spec.js +39 -0
  98. package/dist/client/use-chat-models.spec.js.map +1 -0
  99. package/dist/code-agents/background-controller.d.ts.map +1 -1
  100. package/dist/code-agents/background-controller.js +16 -0
  101. package/dist/code-agents/background-controller.js.map +1 -1
  102. package/dist/code-agents/index.d.ts +2 -0
  103. package/dist/code-agents/index.d.ts.map +1 -1
  104. package/dist/code-agents/index.js +2 -0
  105. package/dist/code-agents/index.js.map +1 -1
  106. package/dist/code-agents/prompt-attachments.d.ts +11 -0
  107. package/dist/code-agents/prompt-attachments.d.ts.map +1 -0
  108. package/dist/code-agents/prompt-attachments.js +23 -0
  109. package/dist/code-agents/prompt-attachments.js.map +1 -0
  110. package/dist/code-agents/transcript-normalizer.js +14 -5
  111. package/dist/code-agents/transcript-normalizer.js.map +1 -1
  112. package/dist/code-agents/transcript-order.d.ts +16 -0
  113. package/dist/code-agents/transcript-order.d.ts.map +1 -0
  114. package/dist/code-agents/transcript-order.js +47 -0
  115. package/dist/code-agents/transcript-order.js.map +1 -0
  116. package/dist/extensions/routes.d.ts.map +1 -1
  117. package/dist/extensions/routes.js +18 -14
  118. package/dist/extensions/routes.js.map +1 -1
  119. package/dist/mcp/build-server.d.ts +33 -1
  120. package/dist/mcp/build-server.d.ts.map +1 -1
  121. package/dist/mcp/build-server.js +39 -12
  122. package/dist/mcp/build-server.js.map +1 -1
  123. package/dist/mcp/builtin-tools.d.ts +3 -1
  124. package/dist/mcp/builtin-tools.d.ts.map +1 -1
  125. package/dist/mcp/builtin-tools.js +40 -10
  126. package/dist/mcp/builtin-tools.js.map +1 -1
  127. package/dist/mcp/connect-route.d.ts.map +1 -1
  128. package/dist/mcp/connect-route.js +299 -97
  129. package/dist/mcp/connect-route.js.map +1 -1
  130. package/dist/mcp/server.d.ts.map +1 -1
  131. package/dist/mcp/server.js +17 -3
  132. package/dist/mcp/server.js.map +1 -1
  133. package/dist/secrets/substitution.d.ts +18 -0
  134. package/dist/secrets/substitution.d.ts.map +1 -1
  135. package/dist/secrets/substitution.js +74 -0
  136. package/dist/secrets/substitution.js.map +1 -1
  137. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  138. package/dist/server/agent-chat-plugin.js +33 -0
  139. package/dist/server/agent-chat-plugin.js.map +1 -1
  140. package/dist/server/auth.d.ts +8 -0
  141. package/dist/server/auth.d.ts.map +1 -1
  142. package/dist/server/auth.js +8 -1
  143. package/dist/server/auth.js.map +1 -1
  144. package/dist/server/deep-link.d.ts +0 -18
  145. package/dist/server/deep-link.d.ts.map +1 -1
  146. package/dist/server/deep-link.js +2 -1
  147. package/dist/server/deep-link.js.map +1 -1
  148. package/dist/server/open-route.d.ts.map +1 -1
  149. package/dist/server/open-route.js +23 -4
  150. package/dist/server/open-route.js.map +1 -1
  151. package/dist/shared/agent-sidebar-url.d.ts +6 -0
  152. package/dist/shared/agent-sidebar-url.d.ts.map +1 -0
  153. package/dist/shared/agent-sidebar-url.js +37 -0
  154. package/dist/shared/agent-sidebar-url.js.map +1 -0
  155. package/dist/shared/index.d.ts +1 -0
  156. package/dist/shared/index.d.ts.map +1 -1
  157. package/dist/shared/index.js +1 -0
  158. package/dist/shared/index.js.map +1 -1
  159. package/dist/styles/agent-conversation.css +403 -0
  160. package/dist/styles/agent-native.css +2 -0
  161. package/dist/vite/client.d.ts.map +1 -1
  162. package/dist/vite/client.js +1 -0
  163. package/dist/vite/client.js.map +1 -1
  164. package/docs/content/external-agents.md +27 -6
  165. package/docs/content/mcp-clients.md +2 -0
  166. package/docs/content/mcp-protocol.md +4 -2
  167. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"connect-route.js","sourceRoot":"","sources":["../../src/mcp/connect-route.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `/_agent-native/mcp/connect` — frictionless external-agent connection.\n *\n * A logged-in user on a deployed agent-native app (e.g. mail.agent-native.com)\n * mints a per-user, scoped, revocable MCP bearer token WITHOUT ever copying a\n * shared deployment secret. Two surfaces:\n *\n * 1. Browser — `GET /connect` renders a minimal in-app page (same inline\n * HTML approach as the auth pages). The Authorize button POSTs to\n * `/connect/token`, then shows the ready-to-paste `.mcp.json` entry, the\n * `agent-native connect <origin>` one-liner, and the user's existing\n * tokens with Revoke buttons.\n * 2. CLI — an OAuth-2.0-device-authorization-style flow:\n * POST /connect/device/start (unauth) → device_code + user_code\n * GET /connect?user_code=… (browser) → user signs in & approves\n * POST /connect/device/authorize (session) → binds user to the code\n * POST /connect/device/poll (unauth) → mints + returns the token\n *\n * The minted token reuses the existing A2A signer (`signA2AToken`) — no new\n * crypto. We only add a random `jti` + `scope: \"mcp-connect\"` claim so it can\n * be revoked. `verifyAuth` already verifies A2A_SECRET JWTs and extracts\n * `sub`/`org_domain`, so a minted token works against `/_agent-native/mcp`\n * with no verify changes for the happy path (the revoke check is the only\n * addition there).\n *\n * Node-only (crypto + the A2A signer), bundled alongside the other framework\n * routes. Dialect-agnostic SQL lives in `connect-store.ts`.\n */\n\nimport type { H3Event } from \"h3\";\nimport { getMethod, getHeader } from \"h3\";\nimport { readBody } from \"../server/h3-helpers.js\";\nimport { getSession, getConfiguredLoginHtml } from \"../server/auth.js\";\nimport { signA2AToken } from \"../a2a/client.js\";\nimport { getOrgDomain } from \"../org/context.js\";\nimport { randomUUID } from \"node:crypto\";\nimport {\n recordMintedToken,\n listTokens,\n revokeToken,\n createDeviceCode,\n getDeviceCode,\n approveDeviceCode,\n claimDeviceCodeForMint,\n finishDeviceCodeMint,\n releaseDeviceCodeMint,\n expireDeviceCode,\n MCP_CONNECT_SCOPE,\n DEFAULT_TOKEN_TTL_DAYS,\n MIN_TOKEN_TTL_DAYS,\n MAX_TOKEN_TTL_DAYS,\n DEVICE_CODE_TTL_MS,\n} from \"./connect-store.js\";\n\n/** Device-flow poll interval hint (seconds). */\nconst DEVICE_POLL_INTERVAL_S = 3;\n\n// Human-typable user code: 8 base32 chars, dashed XXXX-XXXX.\nconst USER_CODE_RE = /^[A-Z2-7]{4}-[A-Z2-7]{4}$/;\n\nexport interface McpConnectRouteOptions {\n /** App id (directory under apps/, e.g. `mail`). Used for the server name. */\n appId?: string;\n /** Human app name shown on the connect page. */\n appName?: string;\n}\n\nfunction json(body: unknown, status = 200): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { \"Content-Type\": \"application/json\" },\n });\n}\n\nfunction html(body: string, status = 200): Response {\n return new Response(body, {\n status,\n headers: { \"Content-Type\": \"text/html; charset=utf-8\" },\n });\n}\n\n/** Derive the running app's origin from request headers (same logic mountMCP\n * uses) — `https` in prod / for non-loopback hosts, `http` for localhost. */\nfunction deriveOrigin(event: H3Event): string {\n const forwardedProto = getHeader(event, \"x-forwarded-proto\");\n const host = getHeader(event, \"x-forwarded-host\") || getHeader(event, \"host\");\n const proto =\n forwardedProto?.split(\",\")[0]?.trim() ||\n (host && /^(localhost|127\\.0\\.0\\.1)(:|$)/.test(host) ? \"http\" : \"https\");\n return host ? `${proto}://${host}` : \"\";\n}\n\nfunction normalizeBasePath(raw: string | undefined): string {\n const trimmed = (raw ?? \"\").trim();\n if (!trimmed || trimmed === \"/\") return \"\";\n const withSlash = trimmed.startsWith(\"/\") ? trimmed : `/${trimmed}`;\n return withSlash.replace(/\\/+$/, \"\");\n}\n\nfunction configuredBasePath(): string {\n return normalizeBasePath(\n process.env.APP_BASE_PATH || process.env.VITE_APP_BASE_PATH,\n );\n}\n\nfunction joinAppPath(basePath: string, path: string): string {\n if (!basePath) return path;\n if (path === \"/\") return basePath;\n return `${basePath}${path.startsWith(\"/\") ? path : `/${path}`}`;\n}\n\nfunction appLabel(origin: string, options: McpConnectRouteOptions): string {\n if (options.appId) return options.appId;\n try {\n const h = new URL(origin).hostname;\n return h.split(\".\")[0] || h;\n } catch {\n return options.appName || \"app\";\n }\n}\n\nfunction serverName(origin: string, options: McpConnectRouteOptions): string {\n return `agent-native-${appLabel(origin, options)}`;\n}\n\nfunction escapeHtml(s: string): string {\n return s\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\");\n}\n\n/**\n * Resolve the org domain for a session. Used as the JWT `org_domain` claim so\n * the receiving MCP endpoint can map it back to an org id (same as A2A). Best\n * effort — a missing org just yields a user-scoped (no-org) token.\n */\nasync function resolveOrgDomain(\n orgId: string | undefined,\n): Promise<string | undefined> {\n if (!orgId) return undefined;\n try {\n return (await getOrgDomain(orgId)) ?? undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction clampTtlDays(input: unknown): number {\n const n = Number(input);\n if (!Number.isFinite(n)) return DEFAULT_TOKEN_TTL_DAYS;\n return Math.min(\n MAX_TOKEN_TTL_DAYS,\n Math.max(MIN_TOKEN_TTL_DAYS, Math.floor(n)),\n );\n}\n\n/**\n * Mint a connect-scoped JWT and record it. The JWT is signed by the existing\n * A2A signer (HS256 over A2A_SECRET); we add a random `jti` and\n * `scope: \"mcp-connect\"` so the token is individually revocable. The token\n * value is returned to the caller exactly once and never persisted.\n */\nasync function mintConnectToken(params: {\n email: string;\n orgId: string | undefined;\n label: string | null;\n ttlDays: number;\n}): Promise<{ token: string; jti: string }> {\n const orgDomain = await resolveOrgDomain(params.orgId);\n const jti = randomUUID();\n // signA2AToken signs { sub: email, org_domain? } over A2A_SECRET (global)\n // or the org secret. We extend its claims via the standard jose builder by\n // re-using the same signer with extra claims threaded through `options`.\n const token = await signA2AToken(params.email, orgDomain, undefined, {\n preferGlobalSecret: true,\n expiresIn: `${params.ttlDays}d`,\n extraClaims: { jti, scope: MCP_CONNECT_SCOPE },\n });\n await recordMintedToken({\n jti,\n ownerEmail: params.email,\n orgId: params.orgId ?? null,\n label: params.label,\n });\n return { token, jti };\n}\n\nfunction mcpResultPayload(\n appUrl: string,\n token: string,\n options: McpConnectRouteOptions,\n) {\n const mcpUrl = `${appUrl}/_agent-native/mcp`;\n const name = serverName(appUrl, options);\n return {\n token,\n mcpUrl,\n serverName: name,\n mcpServerEntry: {\n type: \"http\" as const,\n url: mcpUrl,\n headers: { Authorization: `Bearer ${token}` },\n },\n cli: `agent-native connect ${appUrl}`,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Connect page (server-rendered HTML string)\n// ---------------------------------------------------------------------------\n\nfunction renderConnectPage(params: {\n origin: string;\n connectBasePath: string;\n email: string;\n appName: string;\n userCode: string | null;\n}): string {\n const { origin, connectBasePath, email, appName, userCode } = params;\n const safeOrigin = escapeHtml(origin);\n const safeEmail = escapeHtml(email);\n const safeApp = escapeHtml(appName);\n const safeUserCode =\n userCode && USER_CODE_RE.test(userCode) ? escapeHtml(userCode) : \"\";\n return `<!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>Connect ${safeApp}</title>\n<style>\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n :root {\n color-scheme: dark;\n --bg: #08080a; --panel: #131316; --panel-2: #0d0d10;\n --border: rgba(255,255,255,0.08); --border-strong: rgba(255,255,255,0.14);\n --text: #fafafa; --muted: #a1a1aa; --subtle: #71717a;\n --accent: #fafafa; --accent-fg: #09090b;\n --ring: rgba(250,250,250,0.55);\n --error: #fca5a5; --error-bg: rgba(127,29,29,0.18);\n --ok: #86efac; --ok-bg: rgba(20,83,45,0.2);\n }\n html, body { -webkit-font-smoothing: antialiased; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n background:\n radial-gradient(900px 540px at 50% -14%, rgba(255,255,255,0.05), transparent 60%),\n linear-gradient(180deg, #121215 0%, var(--bg) 56%);\n color: var(--text); display: flex; align-items: center;\n justify-content: center; min-height: 100vh; padding: 1.5rem 1rem;\n }\n .card {\n width: 100%; max-width: 420px;\n background: var(--panel); border: 1px solid var(--border);\n border-radius: 18px; box-shadow: 0 1px 0 rgba(255,255,255,0.04) inset,\n 0 30px 90px rgba(0,0,0,0.5);\n padding: 2.25rem 2rem 1.75rem;\n }\n /* App-to-app glyph */\n .glyph {\n display: flex; align-items: center; justify-content: center;\n gap: 0; margin: 0 auto 1.5rem; width: fit-content;\n }\n .glyph .tile {\n width: 52px; height: 52px; border-radius: 14px;\n display: flex; align-items: center; justify-content: center;\n background: var(--panel-2); border: 1px solid var(--border-strong);\n color: var(--text); flex-shrink: 0;\n }\n .glyph .tile svg { display: block; }\n .glyph .conn {\n width: 38px; height: 2px; flex-shrink: 0;\n background-image: radial-gradient(circle, var(--subtle) 1px, transparent 1.4px);\n background-size: 7px 2px; background-repeat: repeat-x;\n background-position: center;\n }\n .eyebrow {\n text-align: center; font-size: 0.72rem; font-weight: 600;\n letter-spacing: 0.08em; text-transform: uppercase;\n color: var(--subtle); margin-bottom: 0.5rem;\n }\n h1 {\n text-align: center; font-size: 1.4rem; font-weight: 680;\n line-height: 1.25; margin-bottom: 0.55rem;\n letter-spacing: -0.01em;\n }\n .sub {\n text-align: center; color: var(--muted); font-size: 0.9rem;\n line-height: 1.5; margin: 0 auto 0.85rem; max-width: 34ch;\n }\n .identity {\n text-align: center; color: var(--subtle); font-size: 0.78rem;\n margin-bottom: 1.5rem;\n }\n .identity strong { color: var(--muted); font-weight: 600; }\n .code-callout {\n border: 1px solid var(--border-strong); border-radius: 12px;\n padding: 0.85rem 1rem; margin-bottom: 1.25rem;\n background: var(--panel-2); text-align: center;\n }\n .code-callout .label { font-size: 0.68rem; color: var(--subtle);\n text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 0.4rem; }\n .code-callout .value { font-size: 1.6rem; font-weight: 700;\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n letter-spacing: 0.14em; color: var(--text); }\n button {\n cursor: pointer; font: inherit; font-weight: 600; border: none;\n border-radius: 10px; padding: 0.8rem 1.1rem;\n }\n button:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }\n .primary {\n background: var(--accent); color: var(--accent-fg); width: 100%;\n font-size: 0.95rem;\n }\n .primary:hover:not(:disabled) { background: #e4e4e7; }\n .primary:disabled { opacity: 0.55; cursor: default; }\n .ghost {\n background: transparent; color: var(--muted);\n border: 1px solid var(--border-strong); padding: 0.35rem 0.7rem;\n font-size: 0.78rem; font-weight: 500; border-radius: 8px;\n }\n .ghost:hover:not(:disabled) { color: var(--text); border-color: var(--subtle); }\n pre {\n background: var(--panel-2); border: 1px solid var(--border); border-radius: 10px;\n padding: 0.9rem; font-size: 0.78rem; line-height: 1.5; overflow-x: auto;\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n color: #d4d4d8; margin: 0.5rem 0 1rem;\n }\n /* Advanced disclosure */\n .advanced { margin: 0 0 1rem; }\n .advanced > summary {\n list-style: none; cursor: pointer; user-select: none;\n display: flex; align-items: center; justify-content: center; gap: 0.35rem;\n color: var(--subtle); font-size: 0.8rem; font-weight: 500;\n padding: 0.5rem 0; text-align: center;\n }\n .advanced > summary::-webkit-details-marker { display: none; }\n .advanced > summary:hover { color: var(--muted); }\n .advanced > summary:focus-visible { outline: 2px solid var(--ring);\n outline-offset: 2px; border-radius: 6px; }\n .advanced > summary .chev {\n width: 14px; height: 14px; transition: transform 0.15s ease;\n }\n .advanced[open] > summary .chev { transform: rotate(180deg); }\n .advanced-body {\n padding: 0.85rem 0.1rem 0.25rem;\n }\n .field { margin-bottom: 0.9rem; }\n .field:last-child { margin-bottom: 0; }\n .field label { display: block; font-size: 0.78rem; color: var(--muted);\n margin-bottom: 0.35rem; }\n .field input {\n width: 100%; padding: 0.6rem 0.7rem; font: inherit; color: var(--text);\n background: var(--panel-2); border: 1px solid var(--border-strong);\n border-radius: 8px;\n }\n .field input:focus-visible {\n outline: none; border-color: var(--ring);\n box-shadow: 0 0 0 3px rgba(250,250,250,0.12);\n }\n .inline { display: flex; gap: 0.5rem; }\n .inline input { flex: 1; }\n .tokens { margin-top: 1.5rem; border-top: 1px solid var(--border);\n padding-top: 1.25rem; }\n .tokens h2 { font-size: 0.78rem; font-weight: 600; color: var(--muted);\n text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 0.6rem; }\n .tok { display: flex; align-items: center; justify-content: space-between;\n gap: 0.75rem; padding: 0.6rem 0; border-bottom: 1px solid var(--border);\n font-size: 0.83rem; }\n .tok:last-child { border-bottom: none; }\n .tok .meta { color: var(--subtle); font-size: 0.74rem; margin-top: 0.1rem; }\n .tok.revoked { opacity: 0.45; }\n .msg { font-size: 0.83rem; padding: 0.6rem 0.8rem; border-radius: 8px;\n margin-bottom: 1rem; display: none; }\n .msg.err { display: block; color: var(--error); background: var(--error-bg); }\n .msg.ok { display: block; color: var(--ok); background: var(--ok-bg); }\n .hidden { display: none !important; }\n</style>\n</head>\n<body>\n<div class=\"card\">\n <!-- \"Connect an external agent\" — kept as an accessible label / consent eyebrow -->\n <div class=\"glyph\" role=\"img\" aria-label=\"Connect an external agent to ${safeApp}\">\n <span class=\"tile\" aria-hidden=\"true\">\n <svg width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" fill=\"none\">\n <rect x=\"3\" y=\"3\" width=\"8\" height=\"8\" rx=\"2.2\" fill=\"currentColor\"/>\n <rect x=\"13\" y=\"3\" width=\"8\" height=\"8\" rx=\"2.2\" fill=\"currentColor\" opacity=\"0.55\"/>\n <rect x=\"3\" y=\"13\" width=\"8\" height=\"8\" rx=\"2.2\" fill=\"currentColor\" opacity=\"0.55\"/>\n <rect x=\"13\" y=\"13\" width=\"8\" height=\"8\" rx=\"2.2\" fill=\"currentColor\"/>\n </svg>\n </span>\n <span class=\"conn\" aria-hidden=\"true\"></span>\n <span class=\"tile\" aria-hidden=\"true\">\n <svg width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"\n stroke-linejoin=\"round\">\n <path d=\"M9 8 L5 12 L9 16\"/>\n <path d=\"M15 8 L19 12 L15 16\"/>\n </svg>\n </span>\n </div>\n\n <div class=\"eyebrow\">Connect an external agent</div>\n <h1>Authorize ${safeApp}?</h1>\n <p class=\"sub\">Mint a personal token so a coding agent (Claude Code, Codex, Cowork) can act as you on ${safeApp}.</p>\n <p class=\"identity\">Signed in as <strong>${safeEmail}</strong> &middot; ${safeOrigin}</p>\n\n <div id=\"codeCallout\" class=\"code-callout ${safeUserCode ? \"\" : \"hidden\"}\">\n <div class=\"label\">Authorizing device code</div>\n <div class=\"value\" id=\"userCodeValue\">${safeUserCode}</div>\n </div>\n\n <div id=\"msg\" class=\"msg\"></div>\n\n <div id=\"mintForm\">\n <button id=\"authorizeBtn\" class=\"primary\">${safeUserCode ? \"Authorize device\" : \"Create connection token\"}</button>\n <details class=\"advanced\">\n <summary>\n Advanced options\n <svg class=\"chev\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\n aria-hidden=\"true\"><path d=\"M6 9l6 6 6-6\"/></svg>\n </summary>\n <div class=\"advanced-body\">\n <div class=\"field\">\n <label for=\"label\">Label (optional)</label>\n <input id=\"label\" type=\"text\" placeholder=\"e.g. Claude Code on my laptop\" maxlength=\"120\" />\n </div>\n <div class=\"field\">\n <label for=\"ttl\">Expires in (days, 1–365)</label>\n <input id=\"ttl\" type=\"number\" min=\"1\" max=\"365\" value=\"${DEFAULT_TOKEN_TTL_DAYS}\" />\n </div>\n </div>\n </details>\n </div>\n\n <div id=\"result\" class=\"hidden\">\n <p class=\"sub\" id=\"resultMsg\">Token created. Paste this into your agent's MCP config:</p>\n <pre id=\"mcpJson\"></pre>\n <p class=\"sub\">Or from a terminal:</p>\n <pre id=\"cliLine\"></pre>\n </div>\n\n <div class=\"tokens\">\n <h2>Your connections</h2>\n <div id=\"tokenList\"><div class=\"meta\">Loading…</div></div>\n </div>\n</div>\n<script>\n(function () {\n var BASE = ${JSON.stringify(joinAppPath(connectBasePath, \"/_agent-native/mcp/connect\"))};\n var USER_CODE = ${JSON.stringify(safeUserCode || null)};\n var msgEl = document.getElementById(\"msg\");\n function showMsg(text, kind) {\n msgEl.textContent = text;\n msgEl.className = \"msg \" + (kind || \"err\");\n }\n function clearMsg() { msgEl.className = \"msg\"; msgEl.textContent = \"\"; }\n\n function renderResult(data) {\n document.getElementById(\"mintForm\").classList.add(\"hidden\");\n var entry = {};\n entry[data.serverName] = data.mcpServerEntry;\n document.getElementById(\"mcpJson\").textContent =\n JSON.stringify({ mcpServers: entry }, null, 2);\n document.getElementById(\"cliLine\").textContent = data.cli;\n document.getElementById(\"result\").classList.remove(\"hidden\");\n }\n\n async function postJson(path, body) {\n var res = await fetch(BASE + path, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n credentials: \"same-origin\",\n body: JSON.stringify(body || {})\n });\n var data = null;\n try { data = await res.json(); } catch (e) {}\n return { ok: res.ok, status: res.status, data: data };\n }\n\n async function loadTokens() {\n var listEl = document.getElementById(\"tokenList\");\n try {\n var res = await fetch(BASE + \"/tokens\", { credentials: \"same-origin\" });\n if (!res.ok) { listEl.innerHTML = '<div class=\"meta\">Could not load.</div>'; return; }\n var data = await res.json();\n var tokens = (data && data.tokens) || [];\n if (!tokens.length) { listEl.innerHTML = '<div class=\"meta\">No connections yet.</div>'; return; }\n listEl.innerHTML = \"\";\n tokens.forEach(function (t) {\n var div = document.createElement(\"div\");\n div.className = \"tok\" + (t.revokedAt ? \" revoked\" : \"\");\n var when = t.createdAt ? new Date(t.createdAt).toLocaleString() : \"\";\n var used = t.lastUsedAt ? \" · last used \" + new Date(t.lastUsedAt).toLocaleString() : \"\";\n var left = document.createElement(\"div\");\n var label = document.createElement(\"div\");\n label.textContent = t.label || \"(unlabeled)\";\n var meta = document.createElement(\"div\");\n meta.className = \"meta\";\n meta.textContent = (t.revokedAt ? \"Revoked · \" : \"Created \") + when + used;\n left.appendChild(label); left.appendChild(meta);\n div.appendChild(left);\n if (!t.revokedAt) {\n var btn = document.createElement(\"button\");\n btn.className = \"ghost\";\n btn.textContent = \"Revoke\";\n btn.onclick = async function () {\n btn.disabled = true;\n var r = await postJson(\"/tokens/revoke\", { id: t.id });\n if (r.ok) { loadTokens(); }\n else { btn.disabled = false; showMsg(\"Could not revoke token.\"); }\n };\n div.appendChild(btn);\n }\n listEl.appendChild(div);\n });\n } catch (e) {\n listEl.innerHTML = '<div class=\"meta\">Could not load.</div>';\n }\n }\n\n document.getElementById(\"authorizeBtn\").onclick = async function () {\n var btn = this;\n btn.disabled = true;\n clearMsg();\n var label = document.getElementById(\"label\").value || undefined;\n var ttlDays = parseInt(document.getElementById(\"ttl\").value, 10) || undefined;\n try {\n if (USER_CODE) {\n var a = await postJson(\"/device/authorize\", { user_code: USER_CODE });\n if (!a.ok) {\n btn.disabled = false;\n showMsg((a.data && a.data.error) || \"Could not authorize this device code.\");\n return;\n }\n showMsg(\"Device authorized. You can return to your terminal — it will connect automatically.\", \"ok\");\n btn.classList.add(\"hidden\");\n document.getElementById(\"mintForm\").classList.add(\"hidden\");\n } else {\n var m = await postJson(\"/token\", { label: label, ttlDays: ttlDays });\n if (!m.ok) {\n btn.disabled = false;\n showMsg((m.data && m.data.error) || \"Could not create token.\");\n return;\n }\n renderResult(m.data);\n }\n loadTokens();\n } catch (e) {\n btn.disabled = false;\n showMsg(\"Network error. Please try again.\");\n }\n };\n\n loadTokens();\n})();\n</script>\n</body>\n</html>`;\n}\n\n// ---------------------------------------------------------------------------\n// Handler — single entry point; core-routes-plugin dispatches the subpath.\n// ---------------------------------------------------------------------------\n\n/**\n * Handle a `/_agent-native/mcp/connect[...]` request. `subpath` is the part\n * after `/connect` (empty string = the page itself, otherwise e.g.\n * `/token`, `/device/start`). The core-routes-plugin computes it from the\n * stripped event path so this module stays mount-agnostic.\n */\nexport async function handleMcpConnect(\n event: H3Event,\n subpath: string,\n options: McpConnectRouteOptions = {},\n): Promise<Response> {\n const method = getMethod(event);\n const origin = deriveOrigin(event);\n const basePath = configuredBasePath();\n const appUrl = `${origin}${basePath}`;\n const sub = (\"/\" + subpath.replace(/^\\/+/, \"\").replace(/\\/+$/, \"\")).replace(\n /^\\/$/,\n \"\",\n );\n\n // ---- The connect page (GET) ------------------------------------------\n if (sub === \"\") {\n if (method !== \"GET\" && method !== \"HEAD\") {\n return json({ error: \"Method not allowed\" }, 405);\n }\n const session = await getSession(event);\n if (!session?.email) {\n // Serve the SAME login form the guard would, at this same URL — the\n // login form reloads window.location so we re-enter here authed.\n const loginHtml = getConfiguredLoginHtml(event);\n if (loginHtml) return html(loginHtml, 200);\n // Fully-open app (no auth guard): nothing to scope a mint to.\n return html(\n renderConnectPage({\n origin: appUrl,\n connectBasePath: basePath,\n email: \"(no auth configured)\",\n appName: options.appName || appLabel(appUrl, options),\n userCode: null,\n }),\n );\n }\n let userCode: string | null = null;\n try {\n const u = new URL(\n event.node?.req?.url ?? event.path ?? \"/\",\n \"http://an.invalid\",\n );\n const raw = u.searchParams.get(\"user_code\");\n if (raw && USER_CODE_RE.test(raw)) userCode = raw;\n } catch {\n userCode = null;\n }\n return html(\n renderConnectPage({\n origin: appUrl,\n connectBasePath: basePath,\n email: session.email,\n appName: options.appName || appLabel(appUrl, options),\n userCode,\n }),\n );\n }\n\n // ---- POST /token (session-required) ---------------------------------\n if (sub === \"/token\") {\n if (method !== \"POST\") return json({ error: \"Method not allowed\" }, 405);\n const session = await getSession(event);\n if (!session?.email) return json({ error: \"Unauthorized\" }, 401);\n if (!process.env.A2A_SECRET) {\n return json(\n {\n error:\n \"This deployment has no A2A_SECRET configured, so connect tokens cannot be minted.\",\n },\n 503,\n );\n }\n const body = ((await readBody(event).catch(() => ({}))) ?? {}) as {\n label?: unknown;\n ttlDays?: unknown;\n };\n const label =\n typeof body.label === \"string\" && body.label.trim()\n ? body.label.trim().slice(0, 120)\n : null;\n const ttlDays = clampTtlDays(body.ttlDays);\n try {\n const { token } = await mintConnectToken({\n email: session.email,\n orgId: session.orgId,\n label,\n ttlDays,\n });\n return json(mcpResultPayload(appUrl, token, options));\n } catch {\n return json({ error: \"Failed to mint token.\" }, 500);\n }\n }\n\n // ---- POST /device/start (UNAUTH) ------------------------------------\n if (sub === \"/device/start\") {\n if (method !== \"POST\") return json({ error: \"Method not allowed\" }, 405);\n try {\n const row = await createDeviceCode();\n const verificationUri = `${appUrl}/_agent-native/mcp/connect`;\n return json({\n device_code: row.deviceCode,\n user_code: row.userCode,\n verification_uri: verificationUri,\n verification_uri_complete: `${verificationUri}?user_code=${row.userCode}`,\n interval: DEVICE_POLL_INTERVAL_S,\n expires_in: Math.floor(DEVICE_CODE_TTL_MS / 1000),\n });\n } catch (err: any) {\n if (err?.message === \"RATE_LIMITED\") {\n return json({ error: \"Rate limited. Try again shortly.\" }, 429);\n }\n return json({ error: \"Could not start device flow.\" }, 500);\n }\n }\n\n // ---- POST /device/authorize (session-required) ----------------------\n if (sub === \"/device/authorize\") {\n if (method !== \"POST\") return json({ error: \"Method not allowed\" }, 405);\n const session = await getSession(event);\n if (!session?.email) return json({ error: \"Unauthorized\" }, 401);\n const body = ((await readBody(event).catch(() => ({}))) ?? {}) as {\n user_code?: unknown;\n };\n const userCode =\n typeof body.user_code === \"string\" ? body.user_code.trim() : \"\";\n if (!USER_CODE_RE.test(userCode)) {\n return json({ error: \"Invalid user code.\" }, 400);\n }\n const orgId =\n typeof session.orgId === \"string\" && session.orgId.trim()\n ? session.orgId.trim()\n : null;\n const result = await approveDeviceCode(userCode, session.email, orgId);\n if (result === \"not_found\") {\n return json({ error: \"Unknown device code.\" }, 404);\n }\n if (result === \"expired\") {\n return json({ error: \"This device code has expired.\" }, 410);\n }\n if (result === \"already\") {\n return json({ error: \"This device code was already used.\" }, 409);\n }\n return json({ status: \"approved\" });\n }\n\n // ---- POST /device/poll (UNAUTH) -------------------------------------\n if (sub === \"/device/poll\") {\n if (method !== \"POST\") return json({ error: \"Method not allowed\" }, 405);\n const body = ((await readBody(event).catch(() => ({}))) ?? {}) as {\n device_code?: unknown;\n };\n const deviceCode =\n typeof body.device_code === \"string\" ? body.device_code : \"\";\n if (!deviceCode) return json({ error: \"device_code required\" }, 400);\n const row = await getDeviceCode(deviceCode);\n if (!row) return json({ status: \"not_found\" }, 404);\n if (row.status === \"consumed\") return json({ status: \"consumed\" });\n if (\n row.status === \"expired\" ||\n (row.expiresAt != null && row.expiresAt < Date.now())\n ) {\n if (row.status !== \"expired\") void expireDeviceCode(deviceCode);\n return json({ status: \"expired\" });\n }\n if (\n row.status === \"pending\" ||\n row.status === \"minting\" ||\n !row.ownerEmail\n ) {\n return json({ status: \"pending\" });\n }\n // status === \"approved\" && ownerEmail bound → mint exactly once.\n if (!process.env.A2A_SECRET) {\n return json({ status: \"error\", error: \"A2A_SECRET not configured\" }, 503);\n }\n try {\n const jti = randomUUID();\n // Claim a retryable minting state first. If signing or recording fails,\n // release the row back to approved so the CLI can poll again.\n const claimed = await claimDeviceCodeForMint(deviceCode, jti);\n if (!claimed) {\n const fresh = await getDeviceCode(deviceCode);\n if (fresh?.status === \"consumed\") return json({ status: \"consumed\" });\n return json({ status: \"pending\" });\n }\n let token: string;\n try {\n const orgDomain = await resolveOrgDomain(claimed.orgId ?? undefined);\n token = await signA2AToken(claimed.ownerEmail!, orgDomain, undefined, {\n preferGlobalSecret: true,\n expiresIn: `${DEFAULT_TOKEN_TTL_DAYS}d`,\n extraClaims: { jti, scope: MCP_CONNECT_SCOPE },\n });\n await recordMintedToken({\n jti,\n ownerEmail: claimed.ownerEmail!,\n orgId: claimed.orgId,\n label: \"Device connection\",\n });\n if (!(await finishDeviceCodeMint(deviceCode, jti))) {\n return json({ status: \"pending\" });\n }\n } catch (err) {\n await releaseDeviceCodeMint(deviceCode, jti);\n throw err;\n }\n return json({\n status: \"approved\",\n ...mcpResultPayload(appUrl, token, options),\n });\n } catch {\n return json({ status: \"error\", error: \"Failed to mint token.\" }, 500);\n }\n }\n\n // ---- GET /tokens (session-required) ---------------------------------\n if (sub === \"/tokens\") {\n if (method !== \"GET\") return json({ error: \"Method not allowed\" }, 405);\n const session = await getSession(event);\n if (!session?.email) return json({ error: \"Unauthorized\" }, 401);\n const rows = await listTokens(session.email);\n return json({\n tokens: rows.map((r) => ({\n id: r.id,\n label: r.label,\n createdAt: r.createdAt,\n lastUsedAt: r.lastUsedAt,\n revokedAt: r.revokedAt,\n })),\n });\n }\n\n // ---- POST /tokens/revoke (session-required) -------------------------\n if (sub === \"/tokens/revoke\") {\n if (method !== \"POST\") return json({ error: \"Method not allowed\" }, 405);\n const session = await getSession(event);\n if (!session?.email) return json({ error: \"Unauthorized\" }, 401);\n const body = ((await readBody(event).catch(() => ({}))) ?? {}) as {\n id?: unknown;\n };\n const id = typeof body.id === \"string\" ? body.id : \"\";\n if (!id) return json({ error: \"id required\" }, 400);\n const revoked = await revokeToken(session.email, id);\n return json({ ok: revoked });\n }\n\n return json({ error: \"Not found\" }, 404);\n}\n"]}
1
+ {"version":3,"file":"connect-route.js","sourceRoot":"","sources":["../../src/mcp/connect-route.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `/_agent-native/mcp/connect` — frictionless external-agent connection.\n *\n * A logged-in user on a deployed agent-native app (e.g. mail.agent-native.com)\n * mints a per-user, scoped, revocable MCP bearer token WITHOUT ever copying a\n * shared deployment secret. Two surfaces:\n *\n * 1. Browser — `GET /connect` renders a minimal in-app page (same inline\n * HTML approach as the auth pages). The Authorize button POSTs to\n * `/connect/token`, then shows the ready-to-paste `.mcp.json` entry, the\n * `agent-native connect <origin>` one-liner, and the user's existing\n * tokens with Revoke buttons.\n * 2. CLI — an OAuth-2.0-device-authorization-style flow:\n * POST /connect/device/start (unauth) → device_code + user_code\n * GET /connect?user_code=… (browser) → user signs in & approves\n * POST /connect/device/authorize (session) → binds user to the code\n * POST /connect/device/poll (unauth) → mints + returns the token\n *\n * The minted token reuses the existing A2A signer (`signA2AToken`) — no new\n * crypto. We only add a random `jti` + `scope: \"mcp-connect\"` claim so it can\n * be revoked. `verifyAuth` already verifies A2A_SECRET JWTs and extracts\n * `sub`/`org_domain`, so a minted token works against `/_agent-native/mcp`\n * with no verify changes for the happy path (the revoke check is the only\n * addition there).\n *\n * Node-only (crypto + the A2A signer), bundled alongside the other framework\n * routes. Dialect-agnostic SQL lives in `connect-store.ts`.\n */\n\nimport type { H3Event } from \"h3\";\nimport { getMethod, getHeader } from \"h3\";\nimport { readBody } from \"../server/h3-helpers.js\";\nimport {\n getSession,\n getConfiguredLoginHtml,\n isLoopbackRequest,\n} from \"../server/auth.js\";\nimport { signA2AToken } from \"../a2a/client.js\";\nimport { getOrgDomain } from \"../org/context.js\";\nimport { randomUUID } from \"node:crypto\";\nimport {\n recordMintedToken,\n listTokens,\n revokeToken,\n createDeviceCode,\n getDeviceCode,\n approveDeviceCode,\n consumeDeviceCode,\n claimDeviceCodeForMint,\n finishDeviceCodeMint,\n releaseDeviceCodeMint,\n expireDeviceCode,\n MCP_CONNECT_SCOPE,\n DEFAULT_TOKEN_TTL_DAYS,\n MIN_TOKEN_TTL_DAYS,\n MAX_TOKEN_TTL_DAYS,\n DEVICE_CODE_TTL_MS,\n} from \"./connect-store.js\";\n\n/** Device-flow poll interval hint (seconds). */\nconst DEVICE_POLL_INTERVAL_S = 3;\n\n// Human-typable user code: 8 base32 chars, dashed XXXX-XXXX.\nconst USER_CODE_RE = /^[A-Z2-7]{4}-[A-Z2-7]{4}$/;\n\nexport interface McpConnectRouteOptions {\n /** App id (directory under apps/, e.g. `mail`). Used for the server name. */\n appId?: string;\n /** Human app name shown on the connect page. */\n appName?: string;\n}\n\nfunction json(body: unknown, status = 200): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { \"Content-Type\": \"application/json\" },\n });\n}\n\nfunction html(body: string, status = 200): Response {\n return new Response(body, {\n status,\n headers: { \"Content-Type\": \"text/html; charset=utf-8\" },\n });\n}\n\n/** Derive the running app's origin from request headers (same logic mountMCP\n * uses) — `https` in prod / for non-loopback hosts, `http` for localhost. */\nfunction deriveOrigin(event: H3Event): string {\n const forwardedProto = getHeader(event, \"x-forwarded-proto\");\n const host = getHeader(event, \"x-forwarded-host\") || getHeader(event, \"host\");\n const proto =\n forwardedProto?.split(\",\")[0]?.trim() ||\n (host && /^(localhost|127\\.0\\.0\\.1)(:|$)/.test(host) ? \"http\" : \"https\");\n return host ? `${proto}://${host}` : \"\";\n}\n\nfunction normalizeBasePath(raw: string | undefined): string {\n const trimmed = (raw ?? \"\").trim();\n if (!trimmed || trimmed === \"/\") return \"\";\n const withSlash = trimmed.startsWith(\"/\") ? trimmed : `/${trimmed}`;\n return withSlash.replace(/\\/+$/, \"\");\n}\n\nfunction configuredBasePath(): string {\n return normalizeBasePath(\n process.env.APP_BASE_PATH || process.env.VITE_APP_BASE_PATH,\n );\n}\n\nfunction joinAppPath(basePath: string, path: string): string {\n if (!basePath) return path;\n if (path === \"/\") return basePath;\n return `${basePath}${path.startsWith(\"/\") ? path : `/${path}`}`;\n}\n\nfunction appLabel(origin: string, options: McpConnectRouteOptions): string {\n if (options.appId) return options.appId;\n try {\n const h = new URL(origin).hostname;\n return h.split(\".\")[0] || h;\n } catch {\n return options.appName || \"app\";\n }\n}\n\nfunction serverName(origin: string, options: McpConnectRouteOptions): string {\n return `agent-native-${appLabel(origin, options)}`;\n}\n\nfunction canUseDevOpenConnect(event: H3Event): boolean {\n // Loopback determined from the real socket peer (isLoopbackRequest →\n // getRequestIP without xForwardedFor), NOT a parsed `Host` header — the\n // header is client-controlled, and it also handles IPv6 `::1`. A\n // misconfigured public deploy with no secret thus can't unlock dev-open\n // by spoofing `Host: localhost`.\n return (\n isLoopbackRequest(event) &&\n !process.env.A2A_SECRET?.trim() &&\n !process.env.ACCESS_TOKEN?.trim() &&\n !process.env.ACCESS_TOKENS?.trim()\n );\n}\n\nfunction escapeHtml(s: string): string {\n return s\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\");\n}\n\n/**\n * Resolve the org domain for a session. Used as the JWT `org_domain` claim so\n * the receiving MCP endpoint can map it back to an org id (same as A2A). Best\n * effort — a missing org just yields a user-scoped (no-org) token.\n */\nasync function resolveOrgDomain(\n orgId: string | undefined,\n): Promise<string | undefined> {\n if (!orgId) return undefined;\n try {\n return (await getOrgDomain(orgId)) ?? undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction clampTtlDays(input: unknown): number {\n const n = Number(input);\n if (!Number.isFinite(n)) return DEFAULT_TOKEN_TTL_DAYS;\n return Math.min(\n MAX_TOKEN_TTL_DAYS,\n Math.max(MIN_TOKEN_TTL_DAYS, Math.floor(n)),\n );\n}\n\n/**\n * Mint a connect-scoped JWT and record it. The JWT is signed by the existing\n * A2A signer (HS256 over A2A_SECRET); we add a random `jti` and\n * `scope: \"mcp-connect\"` so the token is individually revocable. The token\n * value is returned to the caller exactly once and never persisted.\n */\nasync function mintConnectToken(params: {\n email: string;\n orgId: string | undefined;\n label: string | null;\n ttlDays: number;\n}): Promise<{ token: string; jti: string }> {\n const orgDomain = await resolveOrgDomain(params.orgId);\n const jti = randomUUID();\n // signA2AToken signs { sub: email, org_domain? } over A2A_SECRET (global)\n // or the org secret. We extend its claims via the standard jose builder by\n // re-using the same signer with extra claims threaded through `options`.\n const token = await signA2AToken(params.email, orgDomain, undefined, {\n preferGlobalSecret: true,\n expiresIn: `${params.ttlDays}d`,\n extraClaims: { jti, scope: MCP_CONNECT_SCOPE },\n });\n await recordMintedToken({\n jti,\n ownerEmail: params.email,\n orgId: params.orgId ?? null,\n label: params.label,\n });\n return { token, jti };\n}\n\nfunction mcpResultPayload(\n appUrl: string,\n options: McpConnectRouteOptions,\n auth: { token?: string; ownerEmail?: string },\n) {\n const mcpUrl = `${appUrl}/_agent-native/mcp`;\n const name = serverName(appUrl, options);\n const headers: Record<string, string> = {};\n if (auth.token) headers.Authorization = `Bearer ${auth.token}`;\n if (!auth.token && auth.ownerEmail) {\n headers[\"X-Agent-Native-Owner-Email\"] = auth.ownerEmail;\n }\n return {\n token: auth.token ?? \"\",\n mcpUrl,\n serverName: name,\n mcpServerEntry: {\n type: \"http\" as const,\n url: mcpUrl,\n ...(Object.keys(headers).length ? { headers } : {}),\n },\n cli: `agent-native connect ${appUrl}`,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Connect page (server-rendered HTML string)\n// ---------------------------------------------------------------------------\n\nfunction agentNativeMarkSvg(className: string, gradientId: string): string {\n return `<svg class=\"${className}\" width=\"114\" height=\"66\" viewBox=\"0 0 114 66\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" focusable=\"false\">\n <path d=\"M24.5537 65.7695H0L15.0859 39.4619L37.708 0L60.4912 39.4619H39.6396L24.5537 65.7695Z\" fill=\"white\"/>\n <path d=\"M89.446 0H114L76.2921 65.7704H51.7383L89.446 0Z\" fill=\"url(#${gradientId})\"/>\n <defs>\n <linearGradient id=\"${gradientId}\" x1=\"101.702\" y1=\"67.4791\" x2=\"113.672\" y2=\"-37.4275\" gradientUnits=\"userSpaceOnUse\">\n <stop stop-color=\"#00B5FF\"/>\n <stop offset=\"1\" stop-color=\"#48FFE4\"/>\n </linearGradient>\n </defs>\n</svg>`;\n}\n\nfunction renderConnectPage(params: {\n origin: string;\n connectBasePath: string;\n email: string;\n appName: string;\n userCode: string | null;\n}): string {\n const { origin, connectBasePath, email, appName, userCode } = params;\n const safeOrigin = escapeHtml(origin);\n const safeEmail = escapeHtml(email);\n const safeApp = escapeHtml(appName);\n const brandMarkSvg = agentNativeMarkSvg(\n \"brand-mark\",\n \"agent-native-connect-brand-gradient\",\n );\n const flowMarkSvg = agentNativeMarkSvg(\n \"flow-mark\",\n \"agent-native-connect-flow-gradient\",\n );\n const safeUserCode =\n userCode && USER_CODE_RE.test(userCode) ? escapeHtml(userCode) : \"\";\n return `<!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>Connect ${safeApp}</title>\n<style>\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n :root {\n color-scheme: dark;\n --bg: #09090b; --panel: #121214; --panel-2: #0c0c0e;\n --panel-soft: rgba(255,255,255,0.025);\n --border: rgba(255,255,255,0.075); --border-strong: rgba(255,255,255,0.14);\n --text: #f7f7f8; --muted: #a1a1aa; --subtle: #74747d;\n --accent: #f4f4f5; --accent-fg: #09090b;\n --ring: rgba(250,250,250,0.55);\n --error: #fca5a5; --error-bg: rgba(127,29,29,0.18);\n --ok: #86efac; --ok-bg: rgba(20,83,45,0.12); --ok-border: rgba(134,239,172,0.18);\n }\n html, body { -webkit-font-smoothing: antialiased; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n background: linear-gradient(180deg, #101013 0%, var(--bg) 58%);\n color: var(--text); display: flex; align-items: center;\n justify-content: center; min-height: 100vh; padding: 1.5rem 1rem;\n }\n .card {\n width: 100%; max-width: 440px;\n background: var(--panel); border: 1px solid var(--border);\n border-radius: 8px; box-shadow: 0 1px 0 rgba(255,255,255,0.04) inset,\n 0 30px 90px rgba(0,0,0,0.5);\n padding: 1.25rem;\n }\n .topbar {\n display: flex; align-items: center; justify-content: space-between;\n gap: 0.75rem; margin-bottom: 1.75rem;\n }\n .brand-lockup {\n display: flex; align-items: center; gap: 0.55rem;\n color: var(--muted); font-size: 0.78rem; font-weight: 600;\n }\n .brand-mark { width: 18px; height: auto; display: block; }\n .app-pill {\n max-width: 50%; border: 1px solid var(--border);\n border-radius: 999px; padding: 0.28rem 0.55rem;\n color: var(--subtle); font-size: 0.72rem; line-height: 1;\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n }\n .hero { padding: 0 0.75rem; text-align: center; }\n .flow {\n display: flex; align-items: center; justify-content: center;\n gap: 0; margin: 0 auto 1.1rem; width: fit-content;\n }\n .flow .tile {\n width: 42px; height: 42px; border-radius: 8px;\n display: flex; align-items: center; justify-content: center;\n background: var(--panel-2); border: 1px solid var(--border-strong);\n color: var(--text); flex-shrink: 0;\n }\n .flow-mark { width: 26px; height: auto; display: block; }\n .flow .agent-symbol {\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 0.95rem; font-weight: 700; letter-spacing: -0.04em;\n }\n .flow .conn {\n width: 30px; height: 1px; flex-shrink: 0;\n background: linear-gradient(90deg, transparent, var(--border-strong), transparent);\n background-position: center;\n }\n .eyebrow {\n text-align: center; font-size: 0.72rem; font-weight: 600;\n letter-spacing: 0.08em; text-transform: uppercase;\n color: var(--subtle); margin-bottom: 0.55rem;\n }\n h1 {\n text-align: center; font-size: 1.45rem; font-weight: 680;\n line-height: 1.25; margin-bottom: 0.55rem;\n letter-spacing: -0.01em;\n }\n .sub {\n text-align: center; color: var(--muted); font-size: 0.9rem;\n line-height: 1.5; margin: 0 auto 0.9rem; max-width: 36ch;\n }\n .identity {\n display: flex; flex-wrap: wrap; align-items: center; justify-content: center;\n gap: 0.25rem 0.45rem; color: var(--subtle); font-size: 0.78rem;\n line-height: 1.35; margin: 0 auto 1.4rem; max-width: 34ch;\n }\n .identity strong { color: var(--muted); font-weight: 600; }\n .identity .origin { overflow-wrap: anywhere; }\n .device-strip {\n display: flex; align-items: center; justify-content: space-between;\n gap: 0.75rem; border: 1px solid var(--border);\n border-radius: 8px; padding: 0.55rem 0.65rem; margin: 0 0 0.9rem;\n background: var(--panel-soft); color: var(--muted);\n }\n .device-strip .label {\n font-size: 0.76rem; font-weight: 560; color: var(--subtle);\n }\n .device-strip .value {\n font-size: 0.9rem; font-weight: 700;\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n letter-spacing: 0.08em; color: var(--text);\n }\n button {\n cursor: pointer; font: inherit; font-weight: 600; border: none;\n border-radius: 8px; padding: 0.78rem 1rem;\n }\n button:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }\n .primary {\n background: var(--accent); color: var(--accent-fg); width: 100%;\n font-size: 0.95rem;\n }\n .primary:hover:not(:disabled) { background: #e4e4e7; }\n .primary:disabled { opacity: 0.55; cursor: default; }\n .ghost {\n background: transparent; color: var(--muted);\n border: 1px solid var(--border-strong); padding: 0.35rem 0.7rem;\n font-size: 0.78rem; font-weight: 500; border-radius: 8px;\n }\n .ghost:hover:not(:disabled) { color: var(--text); border-color: var(--subtle); }\n pre {\n background: var(--panel-2); border: 1px solid var(--border); border-radius: 8px;\n padding: 0.9rem; font-size: 0.78rem; line-height: 1.5; overflow-x: auto;\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n color: #d4d4d8; margin: 0.5rem 0 1rem;\n }\n /* Advanced disclosure */\n .advanced { margin: 0 0 1rem; }\n .advanced > summary {\n list-style: none; cursor: pointer; user-select: none;\n display: flex; align-items: center; justify-content: center; gap: 0.35rem;\n color: var(--subtle); font-size: 0.8rem; font-weight: 500;\n padding: 0.5rem 0; text-align: center;\n }\n .advanced > summary::-webkit-details-marker { display: none; }\n .advanced > summary:hover { color: var(--muted); }\n .advanced > summary:focus-visible { outline: 2px solid var(--ring);\n outline-offset: 2px; border-radius: 6px; }\n .advanced > summary .chev {\n width: 7px; height: 7px; border-right: 1.5px solid currentColor;\n border-bottom: 1.5px solid currentColor; transform: rotate(45deg);\n transition: transform 0.15s ease; margin-top: -3px;\n }\n .advanced[open] > summary .chev { transform: rotate(225deg); margin-top: 2px; }\n .advanced-body {\n padding: 0.85rem 0.1rem 0.25rem;\n }\n .field { margin-bottom: 0.9rem; }\n .field:last-child { margin-bottom: 0; }\n .field label { display: block; font-size: 0.78rem; color: var(--muted);\n margin-bottom: 0.35rem; }\n .field input {\n width: 100%; padding: 0.6rem 0.7rem; font: inherit; color: var(--text);\n background: var(--panel-2); border: 1px solid var(--border-strong);\n border-radius: 8px;\n }\n .field input:focus-visible {\n outline: none; border-color: var(--ring);\n box-shadow: 0 0 0 3px rgba(250,250,250,0.12);\n }\n .connections {\n margin-top: 1.1rem; border-top: 1px solid var(--border);\n padding-top: 0.35rem;\n }\n .connections > summary {\n list-style: none; cursor: pointer; user-select: none;\n display: flex; align-items: center; gap: 0.55rem;\n min-height: 2.2rem; color: var(--muted); font-size: 0.82rem;\n }\n .connections > summary::-webkit-details-marker { display: none; }\n .connections > summary:focus-visible {\n outline: 2px solid var(--ring); outline-offset: 2px; border-radius: 6px;\n }\n .connections-title { font-weight: 600; color: var(--muted); }\n .connections-state {\n margin-left: auto; color: var(--subtle); font-size: 0.73rem;\n border: 1px solid var(--border); border-radius: 999px;\n padding: 0.18rem 0.45rem; line-height: 1;\n }\n .connections .chev {\n width: 7px; height: 7px; border-right: 1.5px solid currentColor;\n border-bottom: 1.5px solid currentColor; transform: rotate(45deg);\n transition: transform 0.15s ease; margin: -3px 0 0 0.15rem;\n }\n .connections[open] .chev { transform: rotate(225deg); margin-top: 2px; }\n .token-list { padding-top: 0.4rem; }\n .tok { display: flex; align-items: center; justify-content: space-between;\n gap: 0.75rem; padding: 0.6rem 0; border-bottom: 1px solid var(--border);\n font-size: 0.83rem; }\n .tok:last-child { border-bottom: none; }\n .tok .meta { color: var(--subtle); font-size: 0.74rem; margin-top: 0.1rem; }\n .tok.revoked { opacity: 0.45; }\n .empty-state {\n color: var(--subtle); font-size: 0.78rem; line-height: 1.45;\n padding: 0.3rem 0 0.45rem;\n }\n .msg { font-size: 0.83rem; padding: 0.7rem 0.8rem; border-radius: 8px;\n margin-bottom: 0.9rem; display: none; line-height: 1.4; }\n .msg.err { display: block; color: var(--error); background: var(--error-bg);\n border: 1px solid rgba(252,165,165,0.16); }\n .msg.ok { display: block; color: var(--ok); background: var(--ok-bg);\n border: 1px solid var(--ok-border); }\n .result-panel { padding-top: 0.15rem; }\n .result-title {\n color: var(--text); font-size: 0.95rem; font-weight: 650;\n text-align: center; margin-bottom: 0.35rem;\n }\n .result-copy {\n color: var(--muted); font-size: 0.83rem; line-height: 1.45;\n text-align: center; margin: 0 auto 0.85rem; max-width: 34ch;\n }\n .section-label {\n color: var(--subtle); font-size: 0.7rem; font-weight: 650;\n letter-spacing: 0.08em; text-transform: uppercase; margin-top: 0.85rem;\n }\n @media (max-width: 480px) {\n body { align-items: flex-start; padding: 0.75rem; }\n .card { padding: 1rem; }\n .hero { padding: 0; }\n .topbar { margin-bottom: 1.35rem; }\n h1 { font-size: 1.3rem; }\n .app-pill { max-width: 46%; }\n pre { font-size: 0.72rem; }\n }\n .hidden { display: none !important; }\n</style>\n</head>\n<body>\n<div class=\"card\">\n <div class=\"topbar\">\n <div class=\"brand-lockup\">\n ${brandMarkSvg}\n <span>Agent Native</span>\n </div>\n <div class=\"app-pill\" title=\"${safeApp}\">${safeApp}</div>\n </div>\n\n <div class=\"hero\">\n <!-- \"Connect an external agent\" is kept as the accessible consent label. -->\n <div class=\"flow\" role=\"img\" aria-label=\"Connect an external agent to ${safeApp}\">\n <span class=\"tile\" aria-hidden=\"true\">\n ${flowMarkSvg}\n </span>\n <span class=\"conn\" aria-hidden=\"true\"></span>\n <span class=\"tile\" aria-hidden=\"true\">\n <span class=\"agent-symbol\">&lt;/&gt;</span>\n </span>\n </div>\n\n <div class=\"eyebrow\">Connect an external agent</div>\n <h1>${safeUserCode ? `Authorize ${safeApp} from your terminal?` : `Connect ${safeApp} to an agent`}</h1>\n <p class=\"sub\">Allow Claude Code, Codex, or Cowork to use ${safeApp} with your account. You can revoke access anytime.</p>\n <p class=\"identity\">\n <span>Signed in as <strong>${safeEmail}</strong></span>\n <span aria-hidden=\"true\">&middot;</span>\n <span class=\"origin\">${safeOrigin}</span>\n </p>\n </div>\n\n <div id=\"codeCallout\" class=\"device-strip ${safeUserCode ? \"\" : \"hidden\"}\">\n <span class=\"label\">Device code</span>\n <span class=\"value\" id=\"userCodeValue\">${safeUserCode}</span>\n </div>\n\n <div id=\"msg\" class=\"msg\"></div>\n\n <div id=\"mintForm\">\n <button id=\"authorizeBtn\" class=\"primary\">${safeUserCode ? \"Authorize device\" : \"Create connection token\"}</button>\n <details class=\"advanced\">\n <summary>\n Advanced options\n <span class=\"chev\" aria-hidden=\"true\"></span>\n </summary>\n <div class=\"advanced-body\">\n <div class=\"field\">\n <label for=\"label\">Label (optional)</label>\n <input id=\"label\" type=\"text\" placeholder=\"e.g. Claude Code on my laptop\" maxlength=\"120\" />\n </div>\n <div class=\"field\">\n <label for=\"ttl\">Expires in (days, 1–365)</label>\n <input id=\"ttl\" type=\"number\" min=\"1\" max=\"365\" value=\"${DEFAULT_TOKEN_TTL_DAYS}\" />\n </div>\n </div>\n </details>\n </div>\n\n <div id=\"result\" class=\"result-panel hidden\">\n <div class=\"result-title\">Connection token created</div>\n <p class=\"result-copy\" id=\"resultMsg\">Paste this into your agent's MCP config. The token is shown only once.</p>\n <div class=\"section-label\">MCP config</div>\n <pre id=\"mcpJson\"></pre>\n <details class=\"advanced\">\n <summary>\n Terminal alternative\n <span class=\"chev\" aria-hidden=\"true\"></span>\n </summary>\n <div class=\"advanced-body\">\n <pre id=\"cliLine\"></pre>\n </div>\n </details>\n </div>\n\n <details id=\"connections\" class=\"connections\">\n <summary>\n <span class=\"connections-title\">Existing connections</span>\n <span id=\"connectionsState\" class=\"connections-state\">Checking</span>\n <span class=\"chev\" aria-hidden=\"true\"></span>\n </summary>\n <div id=\"tokenList\" class=\"token-list\"><div class=\"empty-state\">Checking connections...</div></div>\n </details>\n</div>\n<script>\n(function () {\n var BASE = ${JSON.stringify(joinAppPath(connectBasePath, \"/_agent-native/mcp/connect\"))};\n var USER_CODE = ${JSON.stringify(safeUserCode || null)};\n var msgEl = document.getElementById(\"msg\");\n var connectionsEl = document.getElementById(\"connections\");\n var connectionsStateEl = document.getElementById(\"connectionsState\");\n function showMsg(text, kind) {\n msgEl.textContent = text;\n msgEl.className = \"msg \" + (kind || \"err\");\n }\n function clearMsg() { msgEl.className = \"msg\"; msgEl.textContent = \"\"; }\n\n function renderResult(data) {\n document.getElementById(\"mintForm\").classList.add(\"hidden\");\n var entry = {};\n entry[data.serverName] = data.mcpServerEntry;\n document.getElementById(\"mcpJson\").textContent =\n JSON.stringify({ mcpServers: entry }, null, 2);\n document.getElementById(\"cliLine\").textContent = data.cli;\n document.getElementById(\"result\").classList.remove(\"hidden\");\n }\n\n async function postJson(path, body) {\n var res = await fetch(BASE + path, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n credentials: \"same-origin\",\n body: JSON.stringify(body || {})\n });\n var data = null;\n try { data = await res.json(); } catch (e) {}\n return { ok: res.ok, status: res.status, data: data };\n }\n\n async function loadTokens() {\n var listEl = document.getElementById(\"tokenList\");\n try {\n var res = await fetch(BASE + \"/tokens\", { credentials: \"same-origin\" });\n if (!res.ok) {\n connectionsStateEl.textContent = \"Unavailable\";\n connectionsEl.open = true;\n listEl.innerHTML = '<div class=\"empty-state\">Could not load connections.</div>';\n return;\n }\n var data = await res.json();\n var tokens = (data && data.tokens) || [];\n if (!tokens.length) {\n connectionsStateEl.textContent = \"None\";\n connectionsEl.open = false;\n listEl.innerHTML = '<div class=\"empty-state\">Created connections will appear here for revoking later.</div>';\n return;\n }\n var activeCount = tokens.filter(function (t) { return !t.revokedAt; }).length;\n connectionsStateEl.textContent = activeCount === 1 ? \"1 active\" : activeCount + \" active\";\n connectionsEl.open = true;\n listEl.innerHTML = \"\";\n tokens.forEach(function (t) {\n var div = document.createElement(\"div\");\n div.className = \"tok\" + (t.revokedAt ? \" revoked\" : \"\");\n var when = t.createdAt ? new Date(t.createdAt).toLocaleString() : \"\";\n var used = t.lastUsedAt ? \" · last used \" + new Date(t.lastUsedAt).toLocaleString() : \"\";\n var left = document.createElement(\"div\");\n var label = document.createElement(\"div\");\n label.textContent = t.label || \"(unlabeled)\";\n var meta = document.createElement(\"div\");\n meta.className = \"meta\";\n meta.textContent = (t.revokedAt ? \"Revoked · \" : \"Created \") + when + used;\n left.appendChild(label); left.appendChild(meta);\n div.appendChild(left);\n if (!t.revokedAt) {\n var btn = document.createElement(\"button\");\n btn.className = \"ghost\";\n btn.textContent = \"Revoke\";\n btn.onclick = async function () {\n btn.disabled = true;\n var r = await postJson(\"/tokens/revoke\", { id: t.id });\n if (r.ok) { loadTokens(); }\n else { btn.disabled = false; showMsg(\"Could not revoke token.\"); }\n };\n div.appendChild(btn);\n }\n listEl.appendChild(div);\n });\n } catch (e) {\n connectionsStateEl.textContent = \"Unavailable\";\n connectionsEl.open = true;\n listEl.innerHTML = '<div class=\"empty-state\">Could not load connections.</div>';\n }\n }\n\n document.getElementById(\"authorizeBtn\").onclick = async function () {\n var btn = this;\n btn.disabled = true;\n clearMsg();\n var label = document.getElementById(\"label\").value || undefined;\n var ttlDays = parseInt(document.getElementById(\"ttl\").value, 10) || undefined;\n try {\n if (USER_CODE) {\n var a = await postJson(\"/device/authorize\", { user_code: USER_CODE });\n if (!a.ok) {\n btn.disabled = false;\n showMsg((a.data && a.data.error) || \"Could not authorize this device code.\");\n return;\n }\n showMsg(\"Device authorized — finishing connection… you can return to your terminal.\", \"ok\");\n btn.classList.add(\"hidden\");\n document.getElementById(\"mintForm\").classList.add(\"hidden\");\n var cc = document.getElementById(\"codeCallout\");\n if (cc) cc.classList.add(\"hidden\");\n // The token is minted a few seconds later, when the CLI next polls\n // /device/poll — so a single loadTokens() here runs BEFORE the row\n // exists and the list would wrongly read \"No connections yet\" until\n // a manual reload. Snapshot the EXISTING non-revoked token ids first\n // so we announce \"Connected\" only when THIS device's freshly-minted\n // token appears — a user who already has tokens must not get a false\n // success the instant they authorize.\n var priorIds = {};\n try {\n var pr = await fetch(BASE + \"/tokens\", { credentials: \"same-origin\" });\n if (pr.ok) {\n var pd = await pr.json();\n ((pd && pd.tokens) || []).forEach(function (t) {\n if (!t.revokedAt) priorIds[t.id] = true;\n });\n }\n } catch (e) {}\n loadTokens();\n var tries = 0;\n var iv = setInterval(async function () {\n tries++;\n try {\n var res = await fetch(BASE + \"/tokens\", { credentials: \"same-origin\" });\n if (res.ok) {\n var data = await res.json();\n var fresh = ((data && data.tokens) || []).filter(function (t) {\n return !t.revokedAt && !priorIds[t.id];\n });\n if (fresh.length > 0) {\n clearInterval(iv);\n showMsg(\"Connected. This device can now act as you — manage or revoke it below.\", \"ok\");\n loadTokens();\n return;\n }\n }\n } catch (e) {}\n if (tries >= 30) {\n // No new token appeared in the window — e.g. the loopback\n // dev-open path writes a header-only config and never mints.\n // Don't claim \"Connected\" (we couldn't confirm a device token);\n // keep the \"authorized\" message and just refresh the list.\n clearInterval(iv);\n loadTokens();\n }\n }, 2000);\n return;\n } else {\n var m = await postJson(\"/token\", { label: label, ttlDays: ttlDays });\n if (!m.ok) {\n btn.disabled = false;\n showMsg((m.data && m.data.error) || \"Could not create token.\");\n return;\n }\n renderResult(m.data);\n }\n loadTokens();\n } catch (e) {\n btn.disabled = false;\n showMsg(\"Network error. Please try again.\");\n }\n };\n\n loadTokens();\n})();\n</script>\n</body>\n</html>`;\n}\n\n// ---------------------------------------------------------------------------\n// Handler — single entry point; core-routes-plugin dispatches the subpath.\n// ---------------------------------------------------------------------------\n\n/**\n * Handle a `/_agent-native/mcp/connect[...]` request. `subpath` is the part\n * after `/connect` (empty string = the page itself, otherwise e.g.\n * `/token`, `/device/start`). The core-routes-plugin computes it from the\n * stripped event path so this module stays mount-agnostic.\n */\nexport async function handleMcpConnect(\n event: H3Event,\n subpath: string,\n options: McpConnectRouteOptions = {},\n): Promise<Response> {\n const method = getMethod(event);\n const origin = deriveOrigin(event);\n const basePath = configuredBasePath();\n const appUrl = `${origin}${basePath}`;\n const sub = (\"/\" + subpath.replace(/^\\/+/, \"\").replace(/\\/+$/, \"\")).replace(\n /^\\/$/,\n \"\",\n );\n\n // ---- The connect page (GET) ------------------------------------------\n if (sub === \"\") {\n if (method !== \"GET\" && method !== \"HEAD\") {\n return json({ error: \"Method not allowed\" }, 405);\n }\n const session = await getSession(event);\n if (!session?.email) {\n // Serve the SAME login form the guard would, at this same URL — the\n // login form reloads window.location so we re-enter here authed.\n const loginHtml = getConfiguredLoginHtml(event);\n if (loginHtml) return html(loginHtml, 200);\n // Fully-open app (no auth guard): nothing to scope a mint to.\n return html(\n renderConnectPage({\n origin: appUrl,\n connectBasePath: basePath,\n email: \"(no auth configured)\",\n appName: options.appName || appLabel(appUrl, options),\n userCode: null,\n }),\n );\n }\n let userCode: string | null = null;\n try {\n const u = new URL(\n event.node?.req?.url ?? event.path ?? \"/\",\n \"http://an.invalid\",\n );\n const raw = u.searchParams.get(\"user_code\");\n if (raw && USER_CODE_RE.test(raw)) userCode = raw;\n } catch {\n userCode = null;\n }\n return html(\n renderConnectPage({\n origin: appUrl,\n connectBasePath: basePath,\n email: session.email,\n appName: options.appName || appLabel(appUrl, options),\n userCode,\n }),\n );\n }\n\n // ---- POST /token (session-required) ---------------------------------\n if (sub === \"/token\") {\n if (method !== \"POST\") return json({ error: \"Method not allowed\" }, 405);\n const session = await getSession(event);\n if (!session?.email) return json({ error: \"Unauthorized\" }, 401);\n if (!process.env.A2A_SECRET) {\n if (canUseDevOpenConnect(event)) {\n return json(\n mcpResultPayload(appUrl, options, { ownerEmail: session.email }),\n );\n }\n return json(\n {\n error:\n \"This deployment has no A2A_SECRET configured, so connect tokens cannot be minted.\",\n },\n 503,\n );\n }\n const body = ((await readBody(event).catch(() => ({}))) ?? {}) as {\n label?: unknown;\n ttlDays?: unknown;\n };\n const label =\n typeof body.label === \"string\" && body.label.trim()\n ? body.label.trim().slice(0, 120)\n : null;\n const ttlDays = clampTtlDays(body.ttlDays);\n try {\n const { token } = await mintConnectToken({\n email: session.email,\n orgId: session.orgId,\n label,\n ttlDays,\n });\n return json(mcpResultPayload(appUrl, options, { token }));\n } catch {\n return json({ error: \"Failed to mint token.\" }, 500);\n }\n }\n\n // ---- POST /device/start (UNAUTH) ------------------------------------\n if (sub === \"/device/start\") {\n if (method !== \"POST\") return json({ error: \"Method not allowed\" }, 405);\n try {\n const row = await createDeviceCode();\n const verificationUri = `${appUrl}/_agent-native/mcp/connect`;\n return json({\n device_code: row.deviceCode,\n user_code: row.userCode,\n verification_uri: verificationUri,\n verification_uri_complete: `${verificationUri}?user_code=${row.userCode}`,\n interval: DEVICE_POLL_INTERVAL_S,\n expires_in: Math.floor(DEVICE_CODE_TTL_MS / 1000),\n });\n } catch (err: any) {\n if (err?.message === \"RATE_LIMITED\") {\n return json({ error: \"Rate limited. Try again shortly.\" }, 429);\n }\n return json({ error: \"Could not start device flow.\" }, 500);\n }\n }\n\n // ---- POST /device/authorize (session-required) ----------------------\n if (sub === \"/device/authorize\") {\n if (method !== \"POST\") return json({ error: \"Method not allowed\" }, 405);\n const session = await getSession(event);\n if (!session?.email) return json({ error: \"Unauthorized\" }, 401);\n const body = ((await readBody(event).catch(() => ({}))) ?? {}) as {\n user_code?: unknown;\n };\n const userCode =\n typeof body.user_code === \"string\" ? body.user_code.trim() : \"\";\n if (!USER_CODE_RE.test(userCode)) {\n return json({ error: \"Invalid user code.\" }, 400);\n }\n const orgId =\n typeof session.orgId === \"string\" && session.orgId.trim()\n ? session.orgId.trim()\n : null;\n const result = await approveDeviceCode(userCode, session.email, orgId);\n if (result === \"not_found\") {\n return json({ error: \"Unknown device code.\" }, 404);\n }\n if (result === \"expired\") {\n return json({ error: \"This device code has expired.\" }, 410);\n }\n if (result === \"already\") {\n return json({ error: \"This device code was already used.\" }, 409);\n }\n return json({ status: \"approved\" });\n }\n\n // ---- POST /device/poll (UNAUTH) -------------------------------------\n if (sub === \"/device/poll\") {\n if (method !== \"POST\") return json({ error: \"Method not allowed\" }, 405);\n const body = ((await readBody(event).catch(() => ({}))) ?? {}) as {\n device_code?: unknown;\n };\n const deviceCode =\n typeof body.device_code === \"string\" ? body.device_code : \"\";\n if (!deviceCode) return json({ error: \"device_code required\" }, 400);\n const row = await getDeviceCode(deviceCode);\n if (!row) return json({ status: \"not_found\" }, 404);\n if (row.status === \"consumed\") return json({ status: \"consumed\" });\n if (\n row.status === \"expired\" ||\n (row.expiresAt != null && row.expiresAt < Date.now())\n ) {\n if (row.status !== \"expired\") void expireDeviceCode(deviceCode);\n return json({ status: \"expired\" });\n }\n if (\n row.status === \"pending\" ||\n row.status === \"minting\" ||\n !row.ownerEmail\n ) {\n return json({ status: \"pending\" });\n }\n // status === \"approved\" && ownerEmail bound → mint exactly once.\n if (!process.env.A2A_SECRET) {\n if (canUseDevOpenConnect(event)) {\n const consumed = await consumeDeviceCode(\n deviceCode,\n `dev-open-${randomUUID()}`,\n );\n if (!consumed) {\n const fresh = await getDeviceCode(deviceCode);\n if (fresh?.status === \"consumed\") return json({ status: \"consumed\" });\n return json({ status: \"pending\" });\n }\n return json({\n status: \"approved\",\n ...mcpResultPayload(appUrl, options, {\n ownerEmail: row.ownerEmail,\n }),\n });\n }\n return json({ status: \"error\", error: \"A2A_SECRET not configured\" }, 503);\n }\n try {\n const jti = randomUUID();\n // Claim a retryable minting state first. If signing or recording fails,\n // release the row back to approved so the CLI can poll again.\n const claimed = await claimDeviceCodeForMint(deviceCode, jti);\n if (!claimed) {\n const fresh = await getDeviceCode(deviceCode);\n if (fresh?.status === \"consumed\") return json({ status: \"consumed\" });\n return json({ status: \"pending\" });\n }\n let token: string;\n try {\n const orgDomain = await resolveOrgDomain(claimed.orgId ?? undefined);\n token = await signA2AToken(claimed.ownerEmail!, orgDomain, undefined, {\n preferGlobalSecret: true,\n expiresIn: `${DEFAULT_TOKEN_TTL_DAYS}d`,\n extraClaims: { jti, scope: MCP_CONNECT_SCOPE },\n });\n await recordMintedToken({\n jti,\n ownerEmail: claimed.ownerEmail!,\n orgId: claimed.orgId,\n label: \"Device connection\",\n });\n if (!(await finishDeviceCodeMint(deviceCode, jti))) {\n return json({ status: \"pending\" });\n }\n } catch (err) {\n await releaseDeviceCodeMint(deviceCode, jti);\n throw err;\n }\n return json({\n status: \"approved\",\n ...mcpResultPayload(appUrl, options, { token }),\n });\n } catch {\n return json({ status: \"error\", error: \"Failed to mint token.\" }, 500);\n }\n }\n\n // ---- GET /tokens (session-required) ---------------------------------\n if (sub === \"/tokens\") {\n if (method !== \"GET\") return json({ error: \"Method not allowed\" }, 405);\n const session = await getSession(event);\n if (!session?.email) return json({ error: \"Unauthorized\" }, 401);\n const rows = await listTokens(session.email);\n return json({\n tokens: rows.map((r) => ({\n id: r.id,\n label: r.label,\n createdAt: r.createdAt,\n lastUsedAt: r.lastUsedAt,\n revokedAt: r.revokedAt,\n })),\n });\n }\n\n // ---- POST /tokens/revoke (session-required) -------------------------\n if (sub === \"/tokens/revoke\") {\n if (method !== \"POST\") return json({ error: \"Method not allowed\" }, 405);\n const session = await getSession(event);\n if (!session?.email) return json({ error: \"Unauthorized\" }, 401);\n const body = ((await readBody(event).catch(() => ({}))) ?? {}) as {\n id?: unknown;\n };\n const id = typeof body.id === \"string\" ? body.id : \"\";\n if (!id) return json({ error: \"id required\" }, 400);\n const revoked = await revokeToken(session.email, id);\n return json({ ok: revoked });\n }\n\n return json({ error: \"Not found\" }, 404);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AASlC,OAAO,EACL,yBAAyB,EACzB,UAAU,EACV,eAAe,EACf,sBAAsB,EACtB,kBAAkB,EAClB,KAAK,SAAS,EACd,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACpB,MAAM,mBAAmB,CAAC;AAK3B,OAAO,EACL,yBAAyB,EACzB,UAAU,EACV,eAAe,EACf,sBAAsB,EACtB,kBAAkB,GACnB,CAAC;AACF,YAAY,EAAE,SAAS,EAAE,iBAAiB,EAAE,cAAc,EAAE,CAAC;AA2G7D;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,SAAS,GAChB,OAAO,CAAC,QAAQ,GAAG,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAAC,CAgH5D;AAMD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,QAAQ,CACtB,QAAQ,EAAE,GAAG,EACb,MAAM,EAAE,SAAS,EACjB,WAAW,SAAmB,GAC7B,IAAI,CAYN"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAUlC,OAAO,EACL,yBAAyB,EACzB,UAAU,EACV,eAAe,EACf,sBAAsB,EACtB,kBAAkB,EAClB,KAAK,SAAS,EACd,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACpB,MAAM,mBAAmB,CAAC;AAK3B,OAAO,EACL,yBAAyB,EACzB,UAAU,EACV,eAAe,EACf,sBAAsB,EACtB,kBAAkB,GACnB,CAAC;AACF,YAAY,EAAE,SAAS,EAAE,iBAAiB,EAAE,cAAc,EAAE,CAAC;AA2G7D;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,SAAS,GAChB,OAAO,CAAC,QAAQ,GAAG,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAAC,CAyH5D;AAMD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,QAAQ,CACtB,QAAQ,EAAE,GAAG,EACb,MAAM,EAAE,SAAS,EACjB,WAAW,SAAmB,GAC7B,IAAI,CAYN"}
@@ -1,6 +1,7 @@
1
1
  import { getH3App } from "../server/framework-request-handler.js";
2
2
  import { defineEventHandler, setResponseStatus, getMethod, getRequestHeader, } from "h3";
3
3
  import { readBody } from "../server/h3-helpers.js";
4
+ import { isLoopbackRequest } from "../server/auth.js";
4
5
  import { createMCPServerForRequest, verifyAuth, getAccessTokens, resolveOrgIdFromDomain, buildLinkArtifacts, } from "./build-server.js";
5
6
  // Re-export the shared MCP server builder + types so the stdio transport and
6
7
  // any (future) external importer of `@agent-native/core/mcp` keep resolving
@@ -140,7 +141,14 @@ export async function handleMcpRequest(event, config) {
140
141
  // every tool unscoped (userEmail === undefined).
141
142
  const authHeader = getRequestHeader(event, "authorization");
142
143
  const ownerEmailHeader = getRequestHeader(event, "x-agent-native-owner-email");
143
- const authResult = await verifyAuth(authHeader, ownerEmailHeader);
144
+ // Gate header-only dev-open on the REAL socket peer, never a parsed
145
+ // `Host` header (client-controlled — an attacker could send
146
+ // `Host: localhost`). A deployed app missing A2A_SECRET / ACCESS_TOKEN
147
+ // must fail closed rather than trust a spoofable owner-email header that
148
+ // `fullSurface` would otherwise escalate to the full mutating surface.
149
+ const authResult = await verifyAuth(authHeader, ownerEmailHeader, {
150
+ allowDevOpen: isLoopbackRequest(event),
151
+ });
144
152
  if (!authResult.authed) {
145
153
  setResponseStatus(event, 401);
146
154
  return { error: "Unauthorized" };
@@ -160,9 +168,15 @@ export async function handleMcpRequest(event, config) {
160
168
  const body = method === "POST" ? await readBody(event) : undefined;
161
169
  // Per-request stateless transport + server. Both runtimes build the SAME
162
170
  // server from the SAME config + verified identity + request meta, so
163
- // tools/list, tools/call, and the deep-link `_meta` are identical.
171
+ // tools/list, tools/call, and the deep-link `_meta` are identical. A
172
+ // connected real caller (connect-minted token / `mcp install` /
173
+ // ACCESS_TOKEN / production) gets the full action surface even in local
174
+ // dev; unauthenticated dev probes stay sparse. See `external-agents` skill.
164
175
  const requestMeta = deriveRequestMeta(event);
165
- const server = await createMCPServerForRequest(config, authResult.identity, requestMeta);
176
+ const server = await createMCPServerForRequest(config, authResult.identity, {
177
+ ...requestMeta,
178
+ fullSurface: authResult.fullSurface === true,
179
+ });
166
180
  const { nodeReq, nodeRes } = getNodeReqRes(event);
167
181
  if (nodeReq && nodeRes) {
168
182
  // ---- Node fast-path (UNCHANGED behavior) --------------------------------
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"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","sourcesContent":["import type { H3Event } from \"h3\";\nimport { getH3App } from \"../server/framework-request-handler.js\";\nimport {\n defineEventHandler,\n setResponseStatus,\n getMethod,\n getRequestHeader,\n} from \"h3\";\nimport { readBody } from \"../server/h3-helpers.js\";\nimport {\n createMCPServerForRequest,\n verifyAuth,\n getAccessTokens,\n resolveOrgIdFromDomain,\n buildLinkArtifacts,\n type MCPConfig,\n type MCPCallerIdentity,\n type MCPRequestMeta,\n} from \"./build-server.js\";\n\n// Re-export the shared MCP server builder + types so the stdio transport and\n// any (future) external importer of `@agent-native/core/mcp` keep resolving\n// against `./server.js` exactly as before this refactor.\nexport {\n createMCPServerForRequest,\n verifyAuth,\n getAccessTokens,\n resolveOrgIdFromDomain,\n buildLinkArtifacts,\n};\nexport type { MCPConfig, MCPCallerIdentity, MCPRequestMeta };\n\n// ---------------------------------------------------------------------------\n// Runtime detection — Node fast-path vs. web-standard fallback\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve the underlying Node `http` req/res pair if (and only if) we're\n * running on a real Node HTTP server (local dev, `node` Nitro preset). On the\n * web-standard runtime (Nitro 3 / Netlify web runtime, Cloudflare, Deno, Bun)\n * BOTH of these are undefined — that's the signal to take the web fallback\n * instead of returning 501.\n */\nfunction getNodeReqRes(event: H3Event): {\n nodeReq: any | undefined;\n nodeRes: any | undefined;\n} {\n const e = event as any;\n const nodeReq = e.node?.req ?? e.req?.runtime?.node?.req;\n const nodeRes = e.node?.res ?? e.req?.runtime?.node?.res;\n return { nodeReq, nodeRes };\n}\n\n/**\n * Derive the request origin + the markdown deep-link target from the inbound\n * headers. Identical logic for both the Node and web paths so the absolute\n * deep-link URLs in tool results are computed the same way regardless of\n * runtime.\n */\nfunction deriveRequestMeta(event: H3Event): MCPRequestMeta {\n const forwardedProto = getRequestHeader(event, \"x-forwarded-proto\");\n const host = getRequestHeader(event, \"host\");\n const proto =\n forwardedProto?.split(\",\")[0]?.trim() ||\n (host && /^(localhost|127\\.0\\.0\\.1)(:|$)/.test(host) ? \"http\" : \"https\");\n const origin = host ? `${proto}://${host}` : undefined;\n const targetHeader = getRequestHeader(\n event,\n \"x-agent-native-open-target\",\n )?.toLowerCase();\n const target =\n targetHeader === \"desktop\" ||\n targetHeader === \"terminal\" ||\n targetHeader === \"browser\"\n ? (targetHeader as MCPRequestMeta[\"target\"])\n : undefined;\n return { origin, target };\n}\n\n/**\n * Reconstruct a Web Standard `Request` for the web-standard MCP transport.\n *\n * On the web runtime h3 v2 exposes the real web `Request` as `event.req`; we\n * prefer it (its `method` / `headers` are exactly what the client sent). But\n * the framework middleware rewrites `event.req.url` when it strips a mount\n * prefix, and the transport reads `req.method` + `req.headers` (never the\n * body — we pass that via `parsedBody`), so we always synthesize a clean\n * `Request` with the verified method + a fresh `Headers` copy. The URL is\n * cosmetic for the SDK (it only does `new URL(req.url)` for `requestInfo`),\n * so a best-effort absolute URL derived from the inbound host is sufficient\n * and never throws.\n */\nfunction buildWebRequest(event: H3Event, method: string): Request {\n const src = (event as any).req as Request | undefined;\n\n const headers = new Headers();\n if (src?.headers && typeof src.headers.forEach === \"function\") {\n src.headers.forEach((value, key) => headers.set(key, value));\n } else {\n const rawHeaders = (event as any).node?.req?.headers as\n | Record<string, string | string[] | undefined>\n | undefined;\n if (rawHeaders) {\n for (const [key, value] of Object.entries(rawHeaders)) {\n if (value == null) continue;\n headers.set(key, Array.isArray(value) ? value.join(\", \") : value);\n }\n }\n }\n\n // The SDK requires Accept + Content-Type to advertise both JSON and SSE on\n // a POST. Real MCP clients (Claude Code, `agent-native connect`) always\n // send these; we never inject/alter them — if they're absent the SDK\n // returns its spec-mandated 406/415, identical to the Node path.\n\n const host = headers.get(\"host\") || \"localhost\";\n const forwardedProto = headers.get(\"x-forwarded-proto\");\n const proto =\n forwardedProto?.split(\",\")[0]?.trim() ||\n (/^(localhost|127\\.0\\.0\\.1)(:|$)/.test(host) ? \"http\" : \"https\");\n let url = `${proto}://${host}/_agent-native/mcp`;\n try {\n if (src?.url) url = new URL(src.url).href;\n } catch {\n // keep the synthesized URL\n }\n\n // No body here on purpose: the JSON-RPC payload is forwarded via the\n // transport's `parsedBody` option (the same mechanism the Node transport\n // uses), so the request stream is never read twice.\n return new Request(url, { method, headers });\n}\n\n// ---------------------------------------------------------------------------\n// handleMcpRequest — runtime-agnostic MCP request handler\n// ---------------------------------------------------------------------------\n\n/**\n * Handle a single `{routePrefix}/mcp` request on either runtime.\n *\n * - **Node fast-path** (real Node HTTP server): unchanged — delegate to the\n * SDK's `StreamableHTTPServerTransport.handleRequest(nodeReq, nodeRes,\n * body)`, which writes directly to the Node response (full protocol incl.\n * SSE).\n * - **Web-standard fallback** (Nitro 3 / Netlify web runtime, Cloudflare,\n * Deno, Bun — where there is no Node req/res): build the SAME MCP `Server`\n * from the SAME config + identity, drive it through the SDK's\n * `WebStandardStreamableHTTPServerTransport` (which the Node transport is\n * itself just a thin wrapper around), and return the resulting Web\n * `Response` as a normal h3 return value.\n *\n * Auth, the `runWithRequestContext` identity wrap, the deep-link `_meta` /\n * markdown append, `requestMeta` origin/target derivation and the stateless\n * semantics are IDENTICAL on both paths because both build the same server\n * via `createMCPServerForRequest` and both transports funnel into the same\n * `WebStandardStreamableHTTPServerTransport.handleRequest(webRequest, {\n * parsedBody })` with the same options.\n *\n * Returns:\n * - `undefined` when the request targets a sub-route (so management/status\n * routes mounted under `/_agent-native/mcp/*` handle it themselves) — the\n * h3 mount falls through to the next handler.\n * - a Web `Response` (web fallback) or a string/object (Node path /\n * auth-error path) otherwise. The Node path also sets `_handled` so h3\n * doesn't double-write.\n */\nexport async function handleMcpRequest(\n event: H3Event,\n config: MCPConfig,\n): Promise<Response | string | { error: string } | undefined> {\n const pathname = event.url?.pathname || \"/\";\n const subpath = pathname.replace(/^\\/+/, \"\").replace(/\\/+$/, \"\");\n if (subpath) {\n // Let management/status routes mounted under /_agent-native/mcp/* handle\n // their own requests instead of treating them as MCP protocol traffic.\n return undefined;\n }\n\n const method = getMethod(event);\n\n // Auth check — extracts the caller's identity from the JWT (`sub`), or, on\n // the static-token / dev-open path, from the forwarded\n // `X-Agent-Native-Owner-Email` hint the stdio proxy sends (the\n // `agent-native mcp install` flow). Without this the install flow would run\n // every tool unscoped (userEmail === undefined).\n const authHeader = getRequestHeader(event, \"authorization\");\n const ownerEmailHeader = getRequestHeader(\n event,\n \"x-agent-native-owner-email\",\n );\n const authResult = await verifyAuth(authHeader, ownerEmailHeader);\n if (!authResult.authed) {\n setResponseStatus(event, 401);\n return { error: \"Unauthorized\" };\n }\n\n // Stateless mode: only POST is meaningful\n if (method === \"DELETE\") {\n setResponseStatus(event, 204);\n return \"\";\n }\n\n if (method !== \"POST\" && method !== \"GET\") {\n setResponseStatus(event, 405);\n return { error: \"Method not allowed\" };\n }\n\n // Read body for POST (GET has no body). Read it via the h3 helper exactly\n // once; both transports accept it as a pre-parsed body so the request\n // stream is never consumed twice.\n const body = method === \"POST\" ? await readBody(event) : undefined;\n\n // Per-request stateless transport + server. Both runtimes build the SAME\n // server from the SAME config + verified identity + request meta, so\n // tools/list, tools/call, and the deep-link `_meta` are identical.\n const requestMeta = deriveRequestMeta(event);\n const server = await createMCPServerForRequest(\n config,\n authResult.identity,\n requestMeta,\n );\n\n const { nodeReq, nodeRes } = getNodeReqRes(event);\n\n if (nodeReq && nodeRes) {\n // ---- Node fast-path (UNCHANGED behavior) --------------------------------\n const { StreamableHTTPServerTransport } =\n await import(\"@modelcontextprotocol/sdk/server/streamableHttp.js\");\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined, // stateless\n });\n await server.connect(transport);\n try {\n // The SDK transport writes directly to the Node response. Node-only by\n // construction; we only reach here when real Node req/res exist.\n await transport.handleRequest(nodeReq, nodeRes, body);\n } catch (err: any) {\n // The SDK transport writes directly to the Node response. If the socket\n // is already closed/ended (client disconnected, or the host stream\n // layer also flushed), Node throws ERR_STREAM_WRITE_AFTER_END *after*\n // the MCP payload was already delivered correctly. Swallow that benign\n // post-flush write so an external agent disconnecting mid-stream can\n // never take down the server process; rethrow anything else.\n if (err?.code !== \"ERR_STREAM_WRITE_AFTER_END\") throw err;\n if (process.env.DEBUG)\n console.log(\n \"[mcp] ignored post-flush ERR_STREAM_WRITE_AFTER_END (client disconnected)\",\n );\n }\n // Prevent H3 from double-writing the response\n (event as any)._handled = true;\n return undefined;\n }\n\n // ---- Web-standard fallback (Nitro 3 / Netlify web runtime, CF, Deno,\n // Bun) ---------------------------------------------------------------------\n //\n // `StreamableHTTPServerTransport` is itself just a thin wrapper that\n // converts the Node req/res to a web Request/Response and delegates to\n // `WebStandardStreamableHTTPServerTransport.handleRequest(webRequest, {\n // parsedBody })`. Using the web transport directly with the SAME options +\n // the same pre-read `parsedBody` produces byte-identical protocol output\n // (including the deep-link `_meta` built inside createMCPServerForRequest),\n // and works on every web runtime because it returns a Web `Response`\n // (JSON for request/response, or an SSE `ReadableStream` body which h3\n // streams natively).\n const { WebStandardStreamableHTTPServerTransport } =\n await import(\"@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js\");\n const transport = new WebStandardStreamableHTTPServerTransport({\n sessionIdGenerator: undefined, // stateless — same as the Node path\n });\n await server.connect(transport);\n const webRequest = buildWebRequest(event, method);\n // `parsedBody: undefined` would make the SDK try to read `req.json()`; our\n // synthesized request has no body, so only pass the option for POST (where\n // we actually have a parsed body). For GET the transport reads no body.\n const response = await transport.handleRequest(\n webRequest,\n method === \"POST\" ? { parsedBody: body } : undefined,\n );\n return response;\n}\n\n// ---------------------------------------------------------------------------\n// mountMCP — register MCP Streamable HTTP endpoint on H3/Nitro\n// ---------------------------------------------------------------------------\n\n/**\n * Mount an MCP remote server on an H3/Nitro app.\n *\n * Endpoint: `{routePrefix}/mcp` (default `/_agent-native/mcp`)\n *\n * Uses stateless Streamable HTTP transport — no in-memory sessions,\n * compatible with serverless deployments. Runtime-agnostic: a real Node\n * server uses the SDK's Node transport; the web-standard runtime (Nitro 3 /\n * Netlify web runtime, Cloudflare, Deno, Bun) uses the SDK's web-standard\n * transport. Both build the same server and produce identical JSON-RPC\n * output.\n *\n * Auth: Bearer token matching ACCESS_TOKEN/ACCESS_TOKENS or JWT via A2A_SECRET.\n * No auth required when neither is configured (dev mode).\n */\nexport function mountMCP(\n nitroApp: any,\n config: MCPConfig,\n routePrefix = \"/_agent-native\",\n): void {\n getH3App(nitroApp).use(\n `${routePrefix}/mcp`,\n defineEventHandler(async (event) => {\n return handleMcpRequest(event as H3Event, config);\n }),\n );\n\n if (process.env.DEBUG)\n console.log(\n `[mcp] Mounted MCP server at ${routePrefix}/mcp (${Object.keys(config.actions).length} tools${config.askAgent ? \" + ask-agent\" : \"\"})`,\n );\n}\n"]}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"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","sourcesContent":["import type { H3Event } from \"h3\";\nimport { getH3App } from \"../server/framework-request-handler.js\";\nimport {\n defineEventHandler,\n setResponseStatus,\n getMethod,\n getRequestHeader,\n} from \"h3\";\nimport { readBody } from \"../server/h3-helpers.js\";\nimport { isLoopbackRequest } from \"../server/auth.js\";\nimport {\n createMCPServerForRequest,\n verifyAuth,\n getAccessTokens,\n resolveOrgIdFromDomain,\n buildLinkArtifacts,\n type MCPConfig,\n type MCPCallerIdentity,\n type MCPRequestMeta,\n} from \"./build-server.js\";\n\n// Re-export the shared MCP server builder + types so the stdio transport and\n// any (future) external importer of `@agent-native/core/mcp` keep resolving\n// against `./server.js` exactly as before this refactor.\nexport {\n createMCPServerForRequest,\n verifyAuth,\n getAccessTokens,\n resolveOrgIdFromDomain,\n buildLinkArtifacts,\n};\nexport type { MCPConfig, MCPCallerIdentity, MCPRequestMeta };\n\n// ---------------------------------------------------------------------------\n// Runtime detection — Node fast-path vs. web-standard fallback\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve the underlying Node `http` req/res pair if (and only if) we're\n * running on a real Node HTTP server (local dev, `node` Nitro preset). On the\n * web-standard runtime (Nitro 3 / Netlify web runtime, Cloudflare, Deno, Bun)\n * BOTH of these are undefined — that's the signal to take the web fallback\n * instead of returning 501.\n */\nfunction getNodeReqRes(event: H3Event): {\n nodeReq: any | undefined;\n nodeRes: any | undefined;\n} {\n const e = event as any;\n const nodeReq = e.node?.req ?? e.req?.runtime?.node?.req;\n const nodeRes = e.node?.res ?? e.req?.runtime?.node?.res;\n return { nodeReq, nodeRes };\n}\n\n/**\n * Derive the request origin + the markdown deep-link target from the inbound\n * headers. Identical logic for both the Node and web paths so the absolute\n * deep-link URLs in tool results are computed the same way regardless of\n * runtime.\n */\nfunction deriveRequestMeta(event: H3Event): MCPRequestMeta {\n const forwardedProto = getRequestHeader(event, \"x-forwarded-proto\");\n const host = getRequestHeader(event, \"host\");\n const proto =\n forwardedProto?.split(\",\")[0]?.trim() ||\n (host && /^(localhost|127\\.0\\.0\\.1)(:|$)/.test(host) ? \"http\" : \"https\");\n const origin = host ? `${proto}://${host}` : undefined;\n const targetHeader = getRequestHeader(\n event,\n \"x-agent-native-open-target\",\n )?.toLowerCase();\n const target =\n targetHeader === \"desktop\" ||\n targetHeader === \"terminal\" ||\n targetHeader === \"browser\"\n ? (targetHeader as MCPRequestMeta[\"target\"])\n : undefined;\n return { origin, target };\n}\n\n/**\n * Reconstruct a Web Standard `Request` for the web-standard MCP transport.\n *\n * On the web runtime h3 v2 exposes the real web `Request` as `event.req`; we\n * prefer it (its `method` / `headers` are exactly what the client sent). But\n * the framework middleware rewrites `event.req.url` when it strips a mount\n * prefix, and the transport reads `req.method` + `req.headers` (never the\n * body — we pass that via `parsedBody`), so we always synthesize a clean\n * `Request` with the verified method + a fresh `Headers` copy. The URL is\n * cosmetic for the SDK (it only does `new URL(req.url)` for `requestInfo`),\n * so a best-effort absolute URL derived from the inbound host is sufficient\n * and never throws.\n */\nfunction buildWebRequest(event: H3Event, method: string): Request {\n const src = (event as any).req as Request | undefined;\n\n const headers = new Headers();\n if (src?.headers && typeof src.headers.forEach === \"function\") {\n src.headers.forEach((value, key) => headers.set(key, value));\n } else {\n const rawHeaders = (event as any).node?.req?.headers as\n | Record<string, string | string[] | undefined>\n | undefined;\n if (rawHeaders) {\n for (const [key, value] of Object.entries(rawHeaders)) {\n if (value == null) continue;\n headers.set(key, Array.isArray(value) ? value.join(\", \") : value);\n }\n }\n }\n\n // The SDK requires Accept + Content-Type to advertise both JSON and SSE on\n // a POST. Real MCP clients (Claude Code, `agent-native connect`) always\n // send these; we never inject/alter them — if they're absent the SDK\n // returns its spec-mandated 406/415, identical to the Node path.\n\n const host = headers.get(\"host\") || \"localhost\";\n const forwardedProto = headers.get(\"x-forwarded-proto\");\n const proto =\n forwardedProto?.split(\",\")[0]?.trim() ||\n (/^(localhost|127\\.0\\.0\\.1)(:|$)/.test(host) ? \"http\" : \"https\");\n let url = `${proto}://${host}/_agent-native/mcp`;\n try {\n if (src?.url) url = new URL(src.url).href;\n } catch {\n // keep the synthesized URL\n }\n\n // No body here on purpose: the JSON-RPC payload is forwarded via the\n // transport's `parsedBody` option (the same mechanism the Node transport\n // uses), so the request stream is never read twice.\n return new Request(url, { method, headers });\n}\n\n// ---------------------------------------------------------------------------\n// handleMcpRequest — runtime-agnostic MCP request handler\n// ---------------------------------------------------------------------------\n\n/**\n * Handle a single `{routePrefix}/mcp` request on either runtime.\n *\n * - **Node fast-path** (real Node HTTP server): unchanged — delegate to the\n * SDK's `StreamableHTTPServerTransport.handleRequest(nodeReq, nodeRes,\n * body)`, which writes directly to the Node response (full protocol incl.\n * SSE).\n * - **Web-standard fallback** (Nitro 3 / Netlify web runtime, Cloudflare,\n * Deno, Bun — where there is no Node req/res): build the SAME MCP `Server`\n * from the SAME config + identity, drive it through the SDK's\n * `WebStandardStreamableHTTPServerTransport` (which the Node transport is\n * itself just a thin wrapper around), and return the resulting Web\n * `Response` as a normal h3 return value.\n *\n * Auth, the `runWithRequestContext` identity wrap, the deep-link `_meta` /\n * markdown append, `requestMeta` origin/target derivation and the stateless\n * semantics are IDENTICAL on both paths because both build the same server\n * via `createMCPServerForRequest` and both transports funnel into the same\n * `WebStandardStreamableHTTPServerTransport.handleRequest(webRequest, {\n * parsedBody })` with the same options.\n *\n * Returns:\n * - `undefined` when the request targets a sub-route (so management/status\n * routes mounted under `/_agent-native/mcp/*` handle it themselves) — the\n * h3 mount falls through to the next handler.\n * - a Web `Response` (web fallback) or a string/object (Node path /\n * auth-error path) otherwise. The Node path also sets `_handled` so h3\n * doesn't double-write.\n */\nexport async function handleMcpRequest(\n event: H3Event,\n config: MCPConfig,\n): Promise<Response | string | { error: string } | undefined> {\n const pathname = event.url?.pathname || \"/\";\n const subpath = pathname.replace(/^\\/+/, \"\").replace(/\\/+$/, \"\");\n if (subpath) {\n // Let management/status routes mounted under /_agent-native/mcp/* handle\n // their own requests instead of treating them as MCP protocol traffic.\n return undefined;\n }\n\n const method = getMethod(event);\n\n // Auth check — extracts the caller's identity from the JWT (`sub`), or, on\n // the static-token / dev-open path, from the forwarded\n // `X-Agent-Native-Owner-Email` hint the stdio proxy sends (the\n // `agent-native mcp install` flow). Without this the install flow would run\n // every tool unscoped (userEmail === undefined).\n const authHeader = getRequestHeader(event, \"authorization\");\n const ownerEmailHeader = getRequestHeader(\n event,\n \"x-agent-native-owner-email\",\n );\n // Gate header-only dev-open on the REAL socket peer, never a parsed\n // `Host` header (client-controlled — an attacker could send\n // `Host: localhost`). A deployed app missing A2A_SECRET / ACCESS_TOKEN\n // must fail closed rather than trust a spoofable owner-email header that\n // `fullSurface` would otherwise escalate to the full mutating surface.\n const authResult = await verifyAuth(authHeader, ownerEmailHeader, {\n allowDevOpen: isLoopbackRequest(event),\n });\n if (!authResult.authed) {\n setResponseStatus(event, 401);\n return { error: \"Unauthorized\" };\n }\n\n // Stateless mode: only POST is meaningful\n if (method === \"DELETE\") {\n setResponseStatus(event, 204);\n return \"\";\n }\n\n if (method !== \"POST\" && method !== \"GET\") {\n setResponseStatus(event, 405);\n return { error: \"Method not allowed\" };\n }\n\n // Read body for POST (GET has no body). Read it via the h3 helper exactly\n // once; both transports accept it as a pre-parsed body so the request\n // stream is never consumed twice.\n const body = method === \"POST\" ? await readBody(event) : undefined;\n\n // Per-request stateless transport + server. Both runtimes build the SAME\n // server from the SAME config + verified identity + request meta, so\n // tools/list, tools/call, and the deep-link `_meta` are identical. A\n // connected real caller (connect-minted token / `mcp install` /\n // ACCESS_TOKEN / production) gets the full action surface even in local\n // dev; unauthenticated dev probes stay sparse. See `external-agents` skill.\n const requestMeta = deriveRequestMeta(event);\n const server = await createMCPServerForRequest(config, authResult.identity, {\n ...requestMeta,\n fullSurface: authResult.fullSurface === true,\n });\n\n const { nodeReq, nodeRes } = getNodeReqRes(event);\n\n if (nodeReq && nodeRes) {\n // ---- Node fast-path (UNCHANGED behavior) --------------------------------\n const { StreamableHTTPServerTransport } =\n await import(\"@modelcontextprotocol/sdk/server/streamableHttp.js\");\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined, // stateless\n });\n await server.connect(transport);\n try {\n // The SDK transport writes directly to the Node response. Node-only by\n // construction; we only reach here when real Node req/res exist.\n await transport.handleRequest(nodeReq, nodeRes, body);\n } catch (err: any) {\n // The SDK transport writes directly to the Node response. If the socket\n // is already closed/ended (client disconnected, or the host stream\n // layer also flushed), Node throws ERR_STREAM_WRITE_AFTER_END *after*\n // the MCP payload was already delivered correctly. Swallow that benign\n // post-flush write so an external agent disconnecting mid-stream can\n // never take down the server process; rethrow anything else.\n if (err?.code !== \"ERR_STREAM_WRITE_AFTER_END\") throw err;\n if (process.env.DEBUG)\n console.log(\n \"[mcp] ignored post-flush ERR_STREAM_WRITE_AFTER_END (client disconnected)\",\n );\n }\n // Prevent H3 from double-writing the response\n (event as any)._handled = true;\n return undefined;\n }\n\n // ---- Web-standard fallback (Nitro 3 / Netlify web runtime, CF, Deno,\n // Bun) ---------------------------------------------------------------------\n //\n // `StreamableHTTPServerTransport` is itself just a thin wrapper that\n // converts the Node req/res to a web Request/Response and delegates to\n // `WebStandardStreamableHTTPServerTransport.handleRequest(webRequest, {\n // parsedBody })`. Using the web transport directly with the SAME options +\n // the same pre-read `parsedBody` produces byte-identical protocol output\n // (including the deep-link `_meta` built inside createMCPServerForRequest),\n // and works on every web runtime because it returns a Web `Response`\n // (JSON for request/response, or an SSE `ReadableStream` body which h3\n // streams natively).\n const { WebStandardStreamableHTTPServerTransport } =\n await import(\"@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js\");\n const transport = new WebStandardStreamableHTTPServerTransport({\n sessionIdGenerator: undefined, // stateless — same as the Node path\n });\n await server.connect(transport);\n const webRequest = buildWebRequest(event, method);\n // `parsedBody: undefined` would make the SDK try to read `req.json()`; our\n // synthesized request has no body, so only pass the option for POST (where\n // we actually have a parsed body). For GET the transport reads no body.\n const response = await transport.handleRequest(\n webRequest,\n method === \"POST\" ? { parsedBody: body } : undefined,\n );\n return response;\n}\n\n// ---------------------------------------------------------------------------\n// mountMCP — register MCP Streamable HTTP endpoint on H3/Nitro\n// ---------------------------------------------------------------------------\n\n/**\n * Mount an MCP remote server on an H3/Nitro app.\n *\n * Endpoint: `{routePrefix}/mcp` (default `/_agent-native/mcp`)\n *\n * Uses stateless Streamable HTTP transport — no in-memory sessions,\n * compatible with serverless deployments. Runtime-agnostic: a real Node\n * server uses the SDK's Node transport; the web-standard runtime (Nitro 3 /\n * Netlify web runtime, Cloudflare, Deno, Bun) uses the SDK's web-standard\n * transport. Both build the same server and produce identical JSON-RPC\n * output.\n *\n * Auth: Bearer token matching ACCESS_TOKEN/ACCESS_TOKENS or JWT via A2A_SECRET.\n * No auth required when neither is configured (dev mode).\n */\nexport function mountMCP(\n nitroApp: any,\n config: MCPConfig,\n routePrefix = \"/_agent-native\",\n): void {\n getH3App(nitroApp).use(\n `${routePrefix}/mcp`,\n defineEventHandler(async (event) => {\n return handleMcpRequest(event as H3Event, config);\n }),\n );\n\n if (process.env.DEBUG)\n console.log(\n `[mcp] Mounted MCP server at ${routePrefix}/mcp (${Object.keys(config.actions).length} tools${config.askAgent ? \" + ask-agent\" : \"\"})`,\n );\n}\n"]}
@@ -29,6 +29,12 @@ export interface ResolveKeyReferencesResult {
29
29
  resolved: string;
30
30
  usedKeys: string[];
31
31
  secretValues: string[];
32
+ resolvedKeys?: ResolvedKeyReference[];
33
+ }
34
+ export interface ResolvedKeyReference {
35
+ name: string;
36
+ scope: SecretScope;
37
+ scopeId: string;
32
38
  }
33
39
  /**
34
40
  * Resolve `${keys.NAME}` references in `text`. For each reference, looks up
@@ -38,6 +44,17 @@ export interface ResolveKeyReferencesResult {
38
44
  * literal placeholder.
39
45
  */
40
46
  export declare function resolveKeyReferences(text: string, scope: SecretScope, scopeId: string): Promise<ResolveKeyReferencesResult>;
47
+ /**
48
+ * Resolve `${keys.NAME}` for browser extension fetches and other request-bound
49
+ * code paths that should honor the active workspace's shared credential store.
50
+ *
51
+ * Lookup order:
52
+ * 1. user scope for personal overrides
53
+ * 2. active org scope (Dispatch vault sync writes here for org workspaces)
54
+ * 3. active org workspace scope (legacy shared rows)
55
+ * 4. solo workspace scope when no org is active
56
+ */
57
+ export declare function resolveKeyReferencesWithRequestScopes(text: string, userScopeId: string): Promise<ResolveKeyReferencesResult>;
41
58
  /**
42
59
  * Check if a URL is allowed by a key's URL allowlist. Returns true when no
43
60
  * allowlist is configured (permissive default — the allowlist is opt-in).
@@ -58,4 +75,5 @@ export declare function validateUrlAllowlist(url: string, allowlist: string[] |
58
75
  * would refuse — keep them aligned.
59
76
  */
60
77
  export declare function getKeyAllowlist(name: string, scope: SecretScope, scopeId: string): Promise<string[] | null>;
78
+ export declare function getResolvedKeyAllowlist(ref: ResolvedKeyReference): Promise<string[] | null>;
61
79
  //# sourceMappingURL=substitution.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"substitution.d.ts","sourceRoot":"","sources":["../../src/secrets/substitution.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAoBjD,MAAM,WAAW,0BAA0B;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,0BAA0B,CAAC,CA4CrC;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,EAAE,GAAG,IAAI,GACzB,OAAO,CAeT;AAED;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,CAU1B"}
1
+ {"version":3,"file":"substitution.d.ts","sourceRoot":"","sources":["../../src/secrets/substitution.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAoBjD,MAAM,WAAW,0BAA0B;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,CAAC,EAAE,oBAAoB,EAAE,CAAC;CACvC;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,0BAA0B,CAAC,CA4CrC;AAED;;;;;;;;;GASG;AACH,wBAAsB,qCAAqC,CACzD,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,0BAA0B,CAAC,CAmCrC;AAiCD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,EAAE,GAAG,IAAI,GACzB,OAAO,CAeT;AAED;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,CAU1B;AAED,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,oBAAoB,GACxB,OAAO,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,CAO1B"}
@@ -85,6 +85,72 @@ export async function resolveKeyReferences(text, scope, scopeId) {
85
85
  });
86
86
  return { resolved, usedKeys, secretValues };
87
87
  }
88
+ /**
89
+ * Resolve `${keys.NAME}` for browser extension fetches and other request-bound
90
+ * code paths that should honor the active workspace's shared credential store.
91
+ *
92
+ * Lookup order:
93
+ * 1. user scope for personal overrides
94
+ * 2. active org scope (Dispatch vault sync writes here for org workspaces)
95
+ * 3. active org workspace scope (legacy shared rows)
96
+ * 4. solo workspace scope when no org is active
97
+ */
98
+ export async function resolveKeyReferencesWithRequestScopes(text, userScopeId) {
99
+ const usedKeys = [];
100
+ const matches = Array.from(text.matchAll(KEY_REFERENCE_REGEX));
101
+ if (matches.length === 0) {
102
+ return { resolved: text, usedKeys, secretValues: [], resolvedKeys: [] };
103
+ }
104
+ const resolutions = new Map();
105
+ const resolvedKeys = [];
106
+ const secretValues = [];
107
+ for (const match of matches) {
108
+ const name = match[1];
109
+ if (resolutions.has(name))
110
+ continue;
111
+ const result = await readRequestScopedSecret(name, userScopeId);
112
+ if (!result) {
113
+ throw new Error(`Referenced key "${name}" is not defined for this user or active workspace. Create it in Settings or the Dispatch vault before using this extension.`);
114
+ }
115
+ resolutions.set(name, result.value);
116
+ usedKeys.push(name);
117
+ resolvedKeys.push(result.ref);
118
+ if (result.value)
119
+ secretValues.push(result.value);
120
+ }
121
+ const resolved = text.replace(KEY_REFERENCE_REGEX, (_, name) => {
122
+ const value = resolutions.get(name);
123
+ if (value === undefined) {
124
+ throw new Error(`Referenced key "${name}" was not resolved`);
125
+ }
126
+ return value;
127
+ });
128
+ return { resolved, usedKeys, secretValues, resolvedKeys };
129
+ }
130
+ async function readRequestScopedSecret(name, userScopeId) {
131
+ const candidates = requestSecretCandidates(userScopeId);
132
+ for (const ref of candidates) {
133
+ const result = await readAppSecret({ key: name, ...ref });
134
+ if (result)
135
+ return { value: result.value, ref: { name, ...ref } };
136
+ }
137
+ return null;
138
+ }
139
+ function requestSecretCandidates(userScopeId) {
140
+ const orgId = getRequestOrgId();
141
+ if (orgId) {
142
+ return [
143
+ { scope: "user", scopeId: userScopeId },
144
+ { scope: "org", scopeId: orgId },
145
+ { scope: "workspace", scopeId: orgId },
146
+ ];
147
+ }
148
+ const email = getRequestUserEmail() || userScopeId;
149
+ return [
150
+ { scope: "user", scopeId: userScopeId },
151
+ { scope: "workspace", scopeId: `solo:${email}` },
152
+ ];
153
+ }
88
154
  /**
89
155
  * Check if a URL is allowed by a key's URL allowlist. Returns true when no
90
156
  * allowlist is configured (permissive default — the allowlist is opt-in).
@@ -133,6 +199,14 @@ export async function getKeyAllowlist(name, scope, scopeId) {
133
199
  }
134
200
  return meta?.urlAllowlist ?? null;
135
201
  }
202
+ export async function getResolvedKeyAllowlist(ref) {
203
+ const meta = await readAppSecretMeta({
204
+ key: ref.name,
205
+ scope: ref.scope,
206
+ scopeId: ref.scopeId,
207
+ });
208
+ return meta?.urlAllowlist ?? null;
209
+ }
136
210
  function getWorkspaceScopeId(userScopeId) {
137
211
  const orgId = getRequestOrgId();
138
212
  if (orgId)
@@ -1 +1 @@
1
- {"version":3,"file":"substitution.js","sourceRoot":"","sources":["../../src/secrets/substitution.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Server-side key substitution for automation tools.\n *\n * Resolves `${keys.NAME}` references in user-supplied strings (URLs, headers,\n * bodies, etc.) by looking up the named secret at tool-dispatch time. The\n * raw secret value NEVER enters the model's context — substitution happens\n * after the agent emits its tool call and before the request is dispatched.\n *\n * SECURITY — workspace-scope fallback (audit 05 H2):\n *\n * The user→workspace fallback is OPT-IN via the\n * `AGENT_NATIVE_KEYS_WORKSPACE_FALLBACK=1` env flag. Default OFF.\n *\n * When a user (any org member) writes a workspace-scoped `OPENAI_API_KEY`,\n * a default-on fallback would let every other org member's tools that\n * reference `${keys.OPENAI_API_KEY}` start using the malicious key\n * (key-skimming, mirror requests, billing hijack). The previous\n * fix-wave gated workspace-scope WRITES behind an org-admin check; this\n * file is the read-side defense-in-depth.\n *\n * When the env flag is unset, `resolveKeyReferences(\"user\", scopeId)`\n * queries ONLY user-scope rows. Tools/automations that need shared\n * defaults must explicitly look up via `scope: \"workspace\"`. Most\n * installs benefit from the stricter default — opt in only after the\n * org-admin write-gate is verified to be active.\n */\n\nimport { readAppSecret, readAppSecretMeta } from \"./storage.js\";\nimport type { SecretScope } from \"./register.js\";\nimport {\n getRequestOrgId,\n getRequestUserEmail,\n} from \"../server/request-context.js\";\n\nconst KEY_REFERENCE_REGEX = /\\$\\{keys\\.([A-Za-z0-9_-]+)\\}/g;\n\nfunction isWorkspaceFallbackEnabled(): boolean {\n const v = process.env.AGENT_NATIVE_KEYS_WORKSPACE_FALLBACK;\n if (!v) return false;\n const normalized = v.trim().toLowerCase();\n return (\n normalized === \"1\" ||\n normalized === \"true\" ||\n normalized === \"yes\" ||\n normalized === \"on\"\n );\n}\n\nexport interface ResolveKeyReferencesResult {\n resolved: string;\n usedKeys: string[];\n secretValues: string[];\n}\n\n/**\n * Resolve `${keys.NAME}` references in `text`. For each reference, looks up\n * the named secret at the given scope, falling back to workspace-scope when\n * the user-scope row doesn't exist. Throws when a referenced key is missing\n * so the agent receives a clear error rather than dispatching with the\n * literal placeholder.\n */\nexport async function resolveKeyReferences(\n text: string,\n scope: SecretScope,\n scopeId: string,\n): Promise<ResolveKeyReferencesResult> {\n const usedKeys: string[] = [];\n const matches = Array.from(text.matchAll(KEY_REFERENCE_REGEX));\n if (matches.length === 0) {\n return { resolved: text, usedKeys, secretValues: [] };\n }\n\n const resolutions = new Map<string, string>();\n const secretValues: string[] = [];\n const workspaceFallbackEnabled = isWorkspaceFallbackEnabled();\n for (const match of matches) {\n const name = match[1];\n if (resolutions.has(name)) continue;\n\n let result = await readAppSecret({ key: name, scope, scopeId });\n // SECURITY (audit 05 H2): user→workspace fallback is opt-in. Default\n // off prevents one malicious org member from poisoning every other\n // member's `${keys.NAME}` resolution with a workspace-scoped value.\n if (!result && scope === \"user\" && workspaceFallbackEnabled) {\n result = await readAppSecret({\n key: name,\n scope: \"workspace\",\n scopeId: getWorkspaceScopeId(scopeId),\n });\n }\n if (!result) {\n throw new Error(\n `Referenced key \"${name}\" is not defined for scope \"${scope}\". Create it in Settings or via the secrets API before using this automation.`,\n );\n }\n resolutions.set(name, result.value);\n usedKeys.push(name);\n if (result.value) secretValues.push(result.value);\n }\n\n const resolved = text.replace(KEY_REFERENCE_REGEX, (_, name: string) => {\n const value = resolutions.get(name);\n if (value === undefined) {\n throw new Error(`Referenced key \"${name}\" was not resolved`);\n }\n return value;\n });\n\n return { resolved, usedKeys, secretValues };\n}\n\n/**\n * Check if a URL is allowed by a key's URL allowlist. Returns true when no\n * allowlist is configured (permissive default — the allowlist is opt-in).\n *\n * Matching is exact on the URL's origin (scheme + host + port), so an entry\n * like `https://hooks.slack.com` blocks `https://evil.example.com` even if\n * the agent tries to redirect the request elsewhere.\n */\nexport function validateUrlAllowlist(\n url: string,\n allowlist: string[] | null,\n): boolean {\n if (!allowlist || allowlist.length === 0) return true;\n let origin: string;\n try {\n origin = new URL(url).origin;\n } catch {\n return false;\n }\n return allowlist.some((entry) => {\n try {\n return new URL(entry).origin === origin;\n } catch {\n return false;\n }\n });\n}\n\n/**\n * Convenience helper: look up a key's allowlist by name+scope. Returns null\n * when the key doesn't exist or has no allowlist configured.\n *\n * SECURITY: workspace fallback obeys the same opt-in flag as\n * `resolveKeyReferences` so the allowlist check stays consistent with the\n * resolved secret. If a future caller queries the allowlist for a key the\n * resolver wouldn't return, we'd risk allowing requests that the resolver\n * would refuse — keep them aligned.\n */\nexport async function getKeyAllowlist(\n name: string,\n scope: SecretScope,\n scopeId: string,\n): Promise<string[] | null> {\n let meta = await readAppSecretMeta({ key: name, scope, scopeId });\n if (!meta && scope === \"user\" && isWorkspaceFallbackEnabled()) {\n meta = await readAppSecretMeta({\n key: name,\n scope: \"workspace\",\n scopeId: getWorkspaceScopeId(scopeId),\n });\n }\n return meta?.urlAllowlist ?? null;\n}\n\nfunction getWorkspaceScopeId(userScopeId: string): string {\n const orgId = getRequestOrgId();\n if (orgId) return orgId;\n const email = getRequestUserEmail() || userScopeId;\n return `solo:${email}`;\n}\n"]}
1
+ {"version":3,"file":"substitution.js","sourceRoot":"","sources":["../../src/secrets/substitution.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Server-side key substitution for automation tools.\n *\n * Resolves `${keys.NAME}` references in user-supplied strings (URLs, headers,\n * bodies, etc.) by looking up the named secret at tool-dispatch time. The\n * raw secret value NEVER enters the model's context — substitution happens\n * after the agent emits its tool call and before the request is dispatched.\n *\n * SECURITY — workspace-scope fallback (audit 05 H2):\n *\n * The user→workspace fallback is OPT-IN via the\n * `AGENT_NATIVE_KEYS_WORKSPACE_FALLBACK=1` env flag. Default OFF.\n *\n * When a user (any org member) writes a workspace-scoped `OPENAI_API_KEY`,\n * a default-on fallback would let every other org member's tools that\n * reference `${keys.OPENAI_API_KEY}` start using the malicious key\n * (key-skimming, mirror requests, billing hijack). The previous\n * fix-wave gated workspace-scope WRITES behind an org-admin check; this\n * file is the read-side defense-in-depth.\n *\n * When the env flag is unset, `resolveKeyReferences(\"user\", scopeId)`\n * queries ONLY user-scope rows. Tools/automations that need shared\n * defaults must explicitly look up via `scope: \"workspace\"`. Most\n * installs benefit from the stricter default — opt in only after the\n * org-admin write-gate is verified to be active.\n */\n\nimport { readAppSecret, readAppSecretMeta } from \"./storage.js\";\nimport type { SecretScope } from \"./register.js\";\nimport {\n getRequestOrgId,\n getRequestUserEmail,\n} from \"../server/request-context.js\";\n\nconst KEY_REFERENCE_REGEX = /\\$\\{keys\\.([A-Za-z0-9_-]+)\\}/g;\n\nfunction isWorkspaceFallbackEnabled(): boolean {\n const v = process.env.AGENT_NATIVE_KEYS_WORKSPACE_FALLBACK;\n if (!v) return false;\n const normalized = v.trim().toLowerCase();\n return (\n normalized === \"1\" ||\n normalized === \"true\" ||\n normalized === \"yes\" ||\n normalized === \"on\"\n );\n}\n\nexport interface ResolveKeyReferencesResult {\n resolved: string;\n usedKeys: string[];\n secretValues: string[];\n resolvedKeys?: ResolvedKeyReference[];\n}\n\nexport interface ResolvedKeyReference {\n name: string;\n scope: SecretScope;\n scopeId: string;\n}\n\n/**\n * Resolve `${keys.NAME}` references in `text`. For each reference, looks up\n * the named secret at the given scope, falling back to workspace-scope when\n * the user-scope row doesn't exist. Throws when a referenced key is missing\n * so the agent receives a clear error rather than dispatching with the\n * literal placeholder.\n */\nexport async function resolveKeyReferences(\n text: string,\n scope: SecretScope,\n scopeId: string,\n): Promise<ResolveKeyReferencesResult> {\n const usedKeys: string[] = [];\n const matches = Array.from(text.matchAll(KEY_REFERENCE_REGEX));\n if (matches.length === 0) {\n return { resolved: text, usedKeys, secretValues: [] };\n }\n\n const resolutions = new Map<string, string>();\n const secretValues: string[] = [];\n const workspaceFallbackEnabled = isWorkspaceFallbackEnabled();\n for (const match of matches) {\n const name = match[1];\n if (resolutions.has(name)) continue;\n\n let result = await readAppSecret({ key: name, scope, scopeId });\n // SECURITY (audit 05 H2): user→workspace fallback is opt-in. Default\n // off prevents one malicious org member from poisoning every other\n // member's `${keys.NAME}` resolution with a workspace-scoped value.\n if (!result && scope === \"user\" && workspaceFallbackEnabled) {\n result = await readAppSecret({\n key: name,\n scope: \"workspace\",\n scopeId: getWorkspaceScopeId(scopeId),\n });\n }\n if (!result) {\n throw new Error(\n `Referenced key \"${name}\" is not defined for scope \"${scope}\". Create it in Settings or via the secrets API before using this automation.`,\n );\n }\n resolutions.set(name, result.value);\n usedKeys.push(name);\n if (result.value) secretValues.push(result.value);\n }\n\n const resolved = text.replace(KEY_REFERENCE_REGEX, (_, name: string) => {\n const value = resolutions.get(name);\n if (value === undefined) {\n throw new Error(`Referenced key \"${name}\" was not resolved`);\n }\n return value;\n });\n\n return { resolved, usedKeys, secretValues };\n}\n\n/**\n * Resolve `${keys.NAME}` for browser extension fetches and other request-bound\n * code paths that should honor the active workspace's shared credential store.\n *\n * Lookup order:\n * 1. user scope for personal overrides\n * 2. active org scope (Dispatch vault sync writes here for org workspaces)\n * 3. active org workspace scope (legacy shared rows)\n * 4. solo workspace scope when no org is active\n */\nexport async function resolveKeyReferencesWithRequestScopes(\n text: string,\n userScopeId: string,\n): Promise<ResolveKeyReferencesResult> {\n const usedKeys: string[] = [];\n const matches = Array.from(text.matchAll(KEY_REFERENCE_REGEX));\n if (matches.length === 0) {\n return { resolved: text, usedKeys, secretValues: [], resolvedKeys: [] };\n }\n\n const resolutions = new Map<string, string>();\n const resolvedKeys: ResolvedKeyReference[] = [];\n const secretValues: string[] = [];\n for (const match of matches) {\n const name = match[1];\n if (resolutions.has(name)) continue;\n\n const result = await readRequestScopedSecret(name, userScopeId);\n if (!result) {\n throw new Error(\n `Referenced key \"${name}\" is not defined for this user or active workspace. Create it in Settings or the Dispatch vault before using this extension.`,\n );\n }\n resolutions.set(name, result.value);\n usedKeys.push(name);\n resolvedKeys.push(result.ref);\n if (result.value) secretValues.push(result.value);\n }\n\n const resolved = text.replace(KEY_REFERENCE_REGEX, (_, name: string) => {\n const value = resolutions.get(name);\n if (value === undefined) {\n throw new Error(`Referenced key \"${name}\" was not resolved`);\n }\n return value;\n });\n\n return { resolved, usedKeys, secretValues, resolvedKeys };\n}\n\nasync function readRequestScopedSecret(\n name: string,\n userScopeId: string,\n): Promise<{ value: string; ref: ResolvedKeyReference } | null> {\n const candidates = requestSecretCandidates(userScopeId);\n for (const ref of candidates) {\n const result = await readAppSecret({ key: name, ...ref });\n if (result) return { value: result.value, ref: { name, ...ref } };\n }\n return null;\n}\n\nfunction requestSecretCandidates(\n userScopeId: string,\n): Array<{ scope: SecretScope; scopeId: string }> {\n const orgId = getRequestOrgId();\n if (orgId) {\n return [\n { scope: \"user\", scopeId: userScopeId },\n { scope: \"org\", scopeId: orgId },\n { scope: \"workspace\", scopeId: orgId },\n ];\n }\n\n const email = getRequestUserEmail() || userScopeId;\n return [\n { scope: \"user\", scopeId: userScopeId },\n { scope: \"workspace\", scopeId: `solo:${email}` },\n ];\n}\n\n/**\n * Check if a URL is allowed by a key's URL allowlist. Returns true when no\n * allowlist is configured (permissive default — the allowlist is opt-in).\n *\n * Matching is exact on the URL's origin (scheme + host + port), so an entry\n * like `https://hooks.slack.com` blocks `https://evil.example.com` even if\n * the agent tries to redirect the request elsewhere.\n */\nexport function validateUrlAllowlist(\n url: string,\n allowlist: string[] | null,\n): boolean {\n if (!allowlist || allowlist.length === 0) return true;\n let origin: string;\n try {\n origin = new URL(url).origin;\n } catch {\n return false;\n }\n return allowlist.some((entry) => {\n try {\n return new URL(entry).origin === origin;\n } catch {\n return false;\n }\n });\n}\n\n/**\n * Convenience helper: look up a key's allowlist by name+scope. Returns null\n * when the key doesn't exist or has no allowlist configured.\n *\n * SECURITY: workspace fallback obeys the same opt-in flag as\n * `resolveKeyReferences` so the allowlist check stays consistent with the\n * resolved secret. If a future caller queries the allowlist for a key the\n * resolver wouldn't return, we'd risk allowing requests that the resolver\n * would refuse — keep them aligned.\n */\nexport async function getKeyAllowlist(\n name: string,\n scope: SecretScope,\n scopeId: string,\n): Promise<string[] | null> {\n let meta = await readAppSecretMeta({ key: name, scope, scopeId });\n if (!meta && scope === \"user\" && isWorkspaceFallbackEnabled()) {\n meta = await readAppSecretMeta({\n key: name,\n scope: \"workspace\",\n scopeId: getWorkspaceScopeId(scopeId),\n });\n }\n return meta?.urlAllowlist ?? null;\n}\n\nexport async function getResolvedKeyAllowlist(\n ref: ResolvedKeyReference,\n): Promise<string[] | null> {\n const meta = await readAppSecretMeta({\n key: ref.name,\n scope: ref.scope,\n scopeId: ref.scopeId,\n });\n return meta?.urlAllowlist ?? null;\n}\n\nfunction getWorkspaceScopeId(userScopeId: string): string {\n const orgId = getRequestOrgId();\n if (orgId) return orgId;\n const email = getRequestUserEmail() || userScopeId;\n return `solo:${email}`;\n}\n"]}