@agent-native/core 0.18.1 → 0.19.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 (109) hide show
  1. package/README.md +1 -11
  2. package/dist/a2a/client.d.ts +7 -0
  3. package/dist/a2a/client.d.ts.map +1 -1
  4. package/dist/a2a/client.js +3 -0
  5. package/dist/a2a/client.js.map +1 -1
  6. package/dist/cli/connect.d.ts +94 -0
  7. package/dist/cli/connect.d.ts.map +1 -0
  8. package/dist/cli/connect.js +443 -0
  9. package/dist/cli/connect.js.map +1 -0
  10. package/dist/cli/index.js +16 -0
  11. package/dist/cli/index.js.map +1 -1
  12. package/dist/cli/mcp-config-writers.d.ts +71 -0
  13. package/dist/cli/mcp-config-writers.d.ts.map +1 -0
  14. package/dist/cli/mcp-config-writers.js +210 -0
  15. package/dist/cli/mcp-config-writers.js.map +1 -0
  16. package/dist/client/AssistantChat.d.ts.map +1 -1
  17. package/dist/client/AssistantChat.js +11 -63
  18. package/dist/client/AssistantChat.js.map +1 -1
  19. package/dist/client/composer/PromptComposer.d.ts +6 -1
  20. package/dist/client/composer/PromptComposer.d.ts.map +1 -1
  21. package/dist/client/composer/PromptComposer.js +5 -4
  22. package/dist/client/composer/PromptComposer.js.map +1 -1
  23. package/dist/client/composer/TiptapComposer.d.ts +6 -1
  24. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  25. package/dist/client/composer/TiptapComposer.js +20 -10
  26. package/dist/client/composer/TiptapComposer.js.map +1 -1
  27. package/dist/client/conversation/AgentConversation.d.ts +18 -0
  28. package/dist/client/conversation/AgentConversation.d.ts.map +1 -0
  29. package/dist/client/conversation/AgentConversation.js +94 -0
  30. package/dist/client/conversation/AgentConversation.js.map +1 -0
  31. package/dist/client/conversation/AgentConversation.spec.d.ts +2 -0
  32. package/dist/client/conversation/AgentConversation.spec.d.ts.map +1 -0
  33. package/dist/client/conversation/AgentConversation.spec.js +69 -0
  34. package/dist/client/conversation/AgentConversation.spec.js.map +1 -0
  35. package/dist/client/conversation/index.d.ts +4 -0
  36. package/dist/client/conversation/index.d.ts.map +1 -0
  37. package/dist/client/conversation/index.js +3 -0
  38. package/dist/client/conversation/index.js.map +1 -0
  39. package/dist/client/conversation/types.d.ts +54 -0
  40. package/dist/client/conversation/types.d.ts.map +1 -0
  41. package/dist/client/conversation/types.js +2 -0
  42. package/dist/client/conversation/types.js.map +1 -0
  43. package/dist/client/conversation/use-near-bottom-autoscroll.d.ts +15 -0
  44. package/dist/client/conversation/use-near-bottom-autoscroll.d.ts.map +1 -0
  45. package/dist/client/conversation/use-near-bottom-autoscroll.js +66 -0
  46. package/dist/client/conversation/use-near-bottom-autoscroll.js.map +1 -0
  47. package/dist/client/index.d.ts +1 -0
  48. package/dist/client/index.d.ts.map +1 -1
  49. package/dist/client/index.js +1 -0
  50. package/dist/client/index.js.map +1 -1
  51. package/dist/client/resources/ResourceTree.d.ts.map +1 -1
  52. package/dist/client/resources/ResourceTree.js +2 -2
  53. package/dist/client/resources/ResourceTree.js.map +1 -1
  54. package/dist/client/resources/ResourcesPanel.d.ts.map +1 -1
  55. package/dist/client/resources/ResourcesPanel.js +4 -28
  56. package/dist/client/resources/ResourcesPanel.js.map +1 -1
  57. package/dist/code-agents/index.d.ts +1 -0
  58. package/dist/code-agents/index.d.ts.map +1 -1
  59. package/dist/code-agents/index.js +1 -0
  60. package/dist/code-agents/index.js.map +1 -1
  61. package/dist/code-agents/transcript-normalizer.d.ts +50 -0
  62. package/dist/code-agents/transcript-normalizer.d.ts.map +1 -0
  63. package/dist/code-agents/transcript-normalizer.js +356 -0
  64. package/dist/code-agents/transcript-normalizer.js.map +1 -0
  65. package/dist/extensions/schema.d.ts +1 -1
  66. package/dist/mcp/build-server.d.ts.map +1 -1
  67. package/dist/mcp/build-server.js +30 -0
  68. package/dist/mcp/build-server.js.map +1 -1
  69. package/dist/mcp/connect-route.d.ts +43 -0
  70. package/dist/mcp/connect-route.d.ts.map +1 -0
  71. package/dist/mcp/connect-route.js +638 -0
  72. package/dist/mcp/connect-route.js.map +1 -0
  73. package/dist/mcp/connect-store.d.ts +132 -0
  74. package/dist/mcp/connect-store.d.ts.map +1 -0
  75. package/dist/mcp/connect-store.js +434 -0
  76. package/dist/mcp/connect-store.js.map +1 -0
  77. package/dist/server/auth.d.ts +17 -0
  78. package/dist/server/auth.d.ts.map +1 -1
  79. package/dist/server/auth.js +149 -33
  80. package/dist/server/auth.js.map +1 -1
  81. package/dist/server/better-auth-instance.d.ts +43 -0
  82. package/dist/server/better-auth-instance.d.ts.map +1 -1
  83. package/dist/server/better-auth-instance.js +25 -0
  84. package/dist/server/better-auth-instance.js.map +1 -1
  85. package/dist/server/core-routes-plugin.d.ts +12 -0
  86. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  87. package/dist/server/core-routes-plugin.js +42 -0
  88. package/dist/server/core-routes-plugin.js.map +1 -1
  89. package/dist/server/identity-sso-store.d.ts +86 -0
  90. package/dist/server/identity-sso-store.d.ts.map +1 -0
  91. package/dist/server/identity-sso-store.js +243 -0
  92. package/dist/server/identity-sso-store.js.map +1 -0
  93. package/dist/server/identity-sso.d.ts +78 -0
  94. package/dist/server/identity-sso.d.ts.map +1 -0
  95. package/dist/server/identity-sso.js +425 -0
  96. package/dist/server/identity-sso.js.map +1 -0
  97. package/dist/server/index.d.ts +1 -0
  98. package/dist/server/index.d.ts.map +1 -1
  99. package/dist/server/index.js +1 -0
  100. package/dist/server/index.js.map +1 -1
  101. package/dist/server/onboarding-html.d.ts.map +1 -1
  102. package/dist/server/onboarding-html.js +2 -1
  103. package/dist/server/onboarding-html.js.map +1 -1
  104. package/dist/sharing/schema.d.ts +1 -1
  105. package/docs/content/code-agents-ui.md +14 -3
  106. package/docs/content/cross-app-sso.md +118 -0
  107. package/docs/content/external-agents.md +130 -51
  108. package/docs/content/migration-workbench.md +1 -1
  109. package/package.json +2 -1
@@ -0,0 +1,43 @@
1
+ /**
2
+ * `/_agent-native/mcp/connect` — frictionless external-agent connection.
3
+ *
4
+ * A logged-in user on a deployed agent-native app (e.g. mail.agent-native.com)
5
+ * mints a per-user, scoped, revocable MCP bearer token WITHOUT ever copying a
6
+ * shared deployment secret. Two surfaces:
7
+ *
8
+ * 1. Browser — `GET /connect` renders a minimal in-app page (same inline
9
+ * HTML approach as the auth pages). The Authorize button POSTs to
10
+ * `/connect/token`, then shows the ready-to-paste `.mcp.json` entry, the
11
+ * `agent-native connect <origin>` one-liner, and the user's existing
12
+ * tokens with Revoke buttons.
13
+ * 2. CLI — an OAuth-2.0-device-authorization-style flow:
14
+ * POST /connect/device/start (unauth) → device_code + user_code
15
+ * GET /connect?user_code=… (browser) → user signs in & approves
16
+ * POST /connect/device/authorize (session) → binds user to the code
17
+ * POST /connect/device/poll (unauth) → mints + returns the token
18
+ *
19
+ * The minted token reuses the existing A2A signer (`signA2AToken`) — no new
20
+ * crypto. We only add a random `jti` + `scope: "mcp-connect"` claim so it can
21
+ * be revoked. `verifyAuth` already verifies A2A_SECRET JWTs and extracts
22
+ * `sub`/`org_domain`, so a minted token works against `/_agent-native/mcp`
23
+ * with no verify changes for the happy path (the revoke check is the only
24
+ * addition there).
25
+ *
26
+ * Node-only (crypto + the A2A signer), bundled alongside the other framework
27
+ * routes. Dialect-agnostic SQL lives in `connect-store.ts`.
28
+ */
29
+ import type { H3Event } from "h3";
30
+ export interface McpConnectRouteOptions {
31
+ /** App id (directory under apps/, e.g. `mail`). Used for the server name. */
32
+ appId?: string;
33
+ /** Human app name shown on the connect page. */
34
+ appName?: string;
35
+ }
36
+ /**
37
+ * Handle a `/_agent-native/mcp/connect[...]` request. `subpath` is the part
38
+ * after `/connect` (empty string = the page itself, otherwise e.g.
39
+ * `/token`, `/device/start`). The core-routes-plugin computes it from the
40
+ * stripped event path so this module stays mount-agnostic.
41
+ */
42
+ export declare function handleMcpConnect(event: H3Event, subpath: string, options?: McpConnectRouteOptions): Promise<Response>;
43
+ //# sourceMappingURL=connect-route.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connect-route.d.ts","sourceRoot":"","sources":["../../src/mcp/connect-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AA+BlC,MAAM,WAAW,sBAAsB;IACrC,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gDAAgD;IAChD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AA6YD;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,QAAQ,CAAC,CAoPnB"}
@@ -0,0 +1,638 @@
1
+ /**
2
+ * `/_agent-native/mcp/connect` — frictionless external-agent connection.
3
+ *
4
+ * A logged-in user on a deployed agent-native app (e.g. mail.agent-native.com)
5
+ * mints a per-user, scoped, revocable MCP bearer token WITHOUT ever copying a
6
+ * shared deployment secret. Two surfaces:
7
+ *
8
+ * 1. Browser — `GET /connect` renders a minimal in-app page (same inline
9
+ * HTML approach as the auth pages). The Authorize button POSTs to
10
+ * `/connect/token`, then shows the ready-to-paste `.mcp.json` entry, the
11
+ * `agent-native connect <origin>` one-liner, and the user's existing
12
+ * tokens with Revoke buttons.
13
+ * 2. CLI — an OAuth-2.0-device-authorization-style flow:
14
+ * POST /connect/device/start (unauth) → device_code + user_code
15
+ * GET /connect?user_code=… (browser) → user signs in & approves
16
+ * POST /connect/device/authorize (session) → binds user to the code
17
+ * POST /connect/device/poll (unauth) → mints + returns the token
18
+ *
19
+ * The minted token reuses the existing A2A signer (`signA2AToken`) — no new
20
+ * crypto. We only add a random `jti` + `scope: "mcp-connect"` claim so it can
21
+ * be revoked. `verifyAuth` already verifies A2A_SECRET JWTs and extracts
22
+ * `sub`/`org_domain`, so a minted token works against `/_agent-native/mcp`
23
+ * with no verify changes for the happy path (the revoke check is the only
24
+ * addition there).
25
+ *
26
+ * Node-only (crypto + the A2A signer), bundled alongside the other framework
27
+ * routes. Dialect-agnostic SQL lives in `connect-store.ts`.
28
+ */
29
+ import { getMethod, getHeader } from "h3";
30
+ import { readBody } from "../server/h3-helpers.js";
31
+ import { getSession, getConfiguredLoginHtml } from "../server/auth.js";
32
+ import { signA2AToken } from "../a2a/client.js";
33
+ import { getOrgDomain } from "../org/context.js";
34
+ import { randomUUID } from "node:crypto";
35
+ import { recordMintedToken, listTokens, revokeToken, createDeviceCode, getDeviceCode, approveDeviceCode, claimDeviceCodeForMint, finishDeviceCodeMint, releaseDeviceCodeMint, expireDeviceCode, MCP_CONNECT_SCOPE, DEFAULT_TOKEN_TTL_DAYS, MIN_TOKEN_TTL_DAYS, MAX_TOKEN_TTL_DAYS, DEVICE_CODE_TTL_MS, } from "./connect-store.js";
36
+ /** Device-flow poll interval hint (seconds). */
37
+ const DEVICE_POLL_INTERVAL_S = 3;
38
+ // Human-typable user code: 8 base32 chars, dashed XXXX-XXXX.
39
+ const USER_CODE_RE = /^[A-Z2-7]{4}-[A-Z2-7]{4}$/;
40
+ function json(body, status = 200) {
41
+ return new Response(JSON.stringify(body), {
42
+ status,
43
+ headers: { "Content-Type": "application/json" },
44
+ });
45
+ }
46
+ function html(body, status = 200) {
47
+ return new Response(body, {
48
+ status,
49
+ headers: { "Content-Type": "text/html; charset=utf-8" },
50
+ });
51
+ }
52
+ /** Derive the running app's origin from request headers (same logic mountMCP
53
+ * uses) — `https` in prod / for non-loopback hosts, `http` for localhost. */
54
+ function deriveOrigin(event) {
55
+ const forwardedProto = getHeader(event, "x-forwarded-proto");
56
+ const host = getHeader(event, "x-forwarded-host") || getHeader(event, "host");
57
+ const proto = forwardedProto?.split(",")[0]?.trim() ||
58
+ (host && /^(localhost|127\.0\.0\.1)(:|$)/.test(host) ? "http" : "https");
59
+ return host ? `${proto}://${host}` : "";
60
+ }
61
+ function normalizeBasePath(raw) {
62
+ const trimmed = (raw ?? "").trim();
63
+ if (!trimmed || trimmed === "/")
64
+ return "";
65
+ const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
66
+ return withSlash.replace(/\/+$/, "");
67
+ }
68
+ function configuredBasePath() {
69
+ return normalizeBasePath(process.env.APP_BASE_PATH || process.env.VITE_APP_BASE_PATH);
70
+ }
71
+ function joinAppPath(basePath, path) {
72
+ if (!basePath)
73
+ return path;
74
+ if (path === "/")
75
+ return basePath;
76
+ return `${basePath}${path.startsWith("/") ? path : `/${path}`}`;
77
+ }
78
+ function appLabel(origin, options) {
79
+ if (options.appId)
80
+ return options.appId;
81
+ try {
82
+ const h = new URL(origin).hostname;
83
+ return h.split(".")[0] || h;
84
+ }
85
+ catch {
86
+ return options.appName || "app";
87
+ }
88
+ }
89
+ function serverName(origin, options) {
90
+ return `agent-native-${appLabel(origin, options)}`;
91
+ }
92
+ function escapeHtml(s) {
93
+ return s
94
+ .replace(/&/g, "&amp;")
95
+ .replace(/</g, "&lt;")
96
+ .replace(/>/g, "&gt;")
97
+ .replace(/"/g, "&quot;");
98
+ }
99
+ /**
100
+ * Resolve the org domain for a session. Used as the JWT `org_domain` claim so
101
+ * the receiving MCP endpoint can map it back to an org id (same as A2A). Best
102
+ * effort — a missing org just yields a user-scoped (no-org) token.
103
+ */
104
+ async function resolveOrgDomain(orgId) {
105
+ if (!orgId)
106
+ return undefined;
107
+ try {
108
+ return (await getOrgDomain(orgId)) ?? undefined;
109
+ }
110
+ catch {
111
+ return undefined;
112
+ }
113
+ }
114
+ function clampTtlDays(input) {
115
+ const n = Number(input);
116
+ if (!Number.isFinite(n))
117
+ return DEFAULT_TOKEN_TTL_DAYS;
118
+ return Math.min(MAX_TOKEN_TTL_DAYS, Math.max(MIN_TOKEN_TTL_DAYS, Math.floor(n)));
119
+ }
120
+ /**
121
+ * Mint a connect-scoped JWT and record it. The JWT is signed by the existing
122
+ * A2A signer (HS256 over A2A_SECRET); we add a random `jti` and
123
+ * `scope: "mcp-connect"` so the token is individually revocable. The token
124
+ * value is returned to the caller exactly once and never persisted.
125
+ */
126
+ async function mintConnectToken(params) {
127
+ const orgDomain = await resolveOrgDomain(params.orgId);
128
+ const jti = randomUUID();
129
+ // signA2AToken signs { sub: email, org_domain? } over A2A_SECRET (global)
130
+ // or the org secret. We extend its claims via the standard jose builder by
131
+ // re-using the same signer with extra claims threaded through `options`.
132
+ const token = await signA2AToken(params.email, orgDomain, undefined, {
133
+ preferGlobalSecret: true,
134
+ expiresIn: `${params.ttlDays}d`,
135
+ extraClaims: { jti, scope: MCP_CONNECT_SCOPE },
136
+ });
137
+ await recordMintedToken({
138
+ jti,
139
+ ownerEmail: params.email,
140
+ orgId: params.orgId ?? null,
141
+ label: params.label,
142
+ });
143
+ return { token, jti };
144
+ }
145
+ function mcpResultPayload(appUrl, token, options) {
146
+ const mcpUrl = `${appUrl}/_agent-native/mcp`;
147
+ const name = serverName(appUrl, options);
148
+ return {
149
+ token,
150
+ mcpUrl,
151
+ serverName: name,
152
+ mcpServerEntry: {
153
+ type: "http",
154
+ url: mcpUrl,
155
+ headers: { Authorization: `Bearer ${token}` },
156
+ },
157
+ cli: `agent-native connect ${appUrl}`,
158
+ };
159
+ }
160
+ // ---------------------------------------------------------------------------
161
+ // Connect page (server-rendered HTML string)
162
+ // ---------------------------------------------------------------------------
163
+ function renderConnectPage(params) {
164
+ const { origin, connectBasePath, email, appName, userCode } = params;
165
+ const safeOrigin = escapeHtml(origin);
166
+ const safeEmail = escapeHtml(email);
167
+ const safeApp = escapeHtml(appName);
168
+ const safeUserCode = userCode && USER_CODE_RE.test(userCode) ? escapeHtml(userCode) : "";
169
+ return `<!DOCTYPE html>
170
+ <html lang="en">
171
+ <head>
172
+ <meta charset="UTF-8">
173
+ <meta name="viewport" content="width=device-width, initial-scale=1">
174
+ <title>Connect ${safeApp}</title>
175
+ <style>
176
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
177
+ :root {
178
+ color-scheme: dark;
179
+ --bg: #09090b; --panel: #141417; --border: rgba(255,255,255,0.1);
180
+ --text: #f4f4f5; --muted: #a1a1aa; --subtle: #71717a;
181
+ --accent: #f4f4f5; --accent-fg: #09090b;
182
+ --error: #fca5a5; --error-bg: rgba(127,29,29,0.18);
183
+ --ok: #86efac; --ok-bg: rgba(20,83,45,0.2);
184
+ }
185
+ body {
186
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
187
+ background: linear-gradient(180deg, #111114 0%, var(--bg) 58%);
188
+ color: var(--text); display: flex; align-items: center;
189
+ justify-content: center; min-height: 100vh; padding: 1rem;
190
+ }
191
+ .card {
192
+ width: 100%; max-width: 520px; padding: 2rem;
193
+ background: var(--panel); border: 1px solid var(--border);
194
+ border-radius: 12px; box-shadow: 0 24px 80px rgba(0,0,0,0.35);
195
+ }
196
+ h1 { font-size: 1.35rem; font-weight: 650; margin-bottom: 0.35rem; }
197
+ .sub { color: var(--muted); font-size: 0.9rem; margin-bottom: 1.25rem; }
198
+ .row { color: var(--subtle); font-size: 0.8rem; margin-bottom: 1.25rem; }
199
+ .code-callout {
200
+ border: 1px solid var(--border); border-radius: 8px; padding: 0.85rem 1rem;
201
+ margin-bottom: 1.25rem; background: rgba(255,255,255,0.03);
202
+ }
203
+ .code-callout .label { font-size: 0.72rem; color: var(--subtle);
204
+ text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 0.35rem; }
205
+ .code-callout .value { font-size: 1.5rem; font-weight: 700;
206
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
207
+ letter-spacing: 0.08em; }
208
+ button {
209
+ cursor: pointer; font: inherit; font-weight: 600; border: none;
210
+ border-radius: 8px; padding: 0.7rem 1.1rem;
211
+ }
212
+ .primary { background: var(--accent); color: var(--accent-fg); width: 100%; }
213
+ .primary:disabled { opacity: 0.6; cursor: default; }
214
+ .ghost {
215
+ background: transparent; color: var(--muted);
216
+ border: 1px solid var(--border); padding: 0.35rem 0.7rem;
217
+ font-size: 0.78rem; font-weight: 500;
218
+ }
219
+ pre {
220
+ background: #0c0c0e; border: 1px solid var(--border); border-radius: 8px;
221
+ padding: 0.9rem; font-size: 0.78rem; line-height: 1.5; overflow-x: auto;
222
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
223
+ color: #d4d4d8; margin: 0.5rem 0 1rem;
224
+ }
225
+ .field { margin-bottom: 1rem; }
226
+ .field label { display: block; font-size: 0.8rem; color: var(--muted);
227
+ margin-bottom: 0.35rem; }
228
+ .field input {
229
+ width: 100%; padding: 0.55rem 0.7rem; font: inherit; color: var(--text);
230
+ background: #0c0c0e; border: 1px solid var(--border); border-radius: 6px;
231
+ }
232
+ .inline { display: flex; gap: 0.5rem; }
233
+ .inline input { flex: 1; }
234
+ .tokens { margin-top: 1.75rem; border-top: 1px solid var(--border);
235
+ padding-top: 1.25rem; }
236
+ .tokens h2 { font-size: 0.95rem; font-weight: 600; margin-bottom: 0.75rem; }
237
+ .tok { display: flex; align-items: center; justify-content: space-between;
238
+ gap: 0.75rem; padding: 0.55rem 0; border-bottom: 1px solid var(--border);
239
+ font-size: 0.83rem; }
240
+ .tok:last-child { border-bottom: none; }
241
+ .tok .meta { color: var(--subtle); font-size: 0.74rem; }
242
+ .tok.revoked { opacity: 0.45; }
243
+ .msg { font-size: 0.83rem; padding: 0.6rem 0.8rem; border-radius: 6px;
244
+ margin-bottom: 1rem; display: none; }
245
+ .msg.err { display: block; color: var(--error); background: var(--error-bg); }
246
+ .msg.ok { display: block; color: var(--ok); background: var(--ok-bg); }
247
+ .hidden { display: none !important; }
248
+ </style>
249
+ </head>
250
+ <body>
251
+ <div class="card">
252
+ <h1>Connect an external agent</h1>
253
+ <div class="sub">Mint a personal token for <strong>${safeApp}</strong> so a coding agent (Claude Code, Codex, Cowork) can act as you.</div>
254
+ <div class="row">Signed in as ${safeEmail} &middot; ${safeOrigin}</div>
255
+
256
+ <div id="codeCallout" class="code-callout ${safeUserCode ? "" : "hidden"}">
257
+ <div class="label">Authorizing device code</div>
258
+ <div class="value" id="userCodeValue">${safeUserCode}</div>
259
+ </div>
260
+
261
+ <div id="msg" class="msg"></div>
262
+
263
+ <div id="mintForm">
264
+ <div class="field">
265
+ <label for="label">Label (optional)</label>
266
+ <input id="label" type="text" placeholder="e.g. Claude Code on my laptop" maxlength="120" />
267
+ </div>
268
+ <div class="field">
269
+ <label for="ttl">Expires in (days, 1–365)</label>
270
+ <input id="ttl" type="number" min="1" max="365" value="${DEFAULT_TOKEN_TTL_DAYS}" />
271
+ </div>
272
+ <button id="authorizeBtn" class="primary">${safeUserCode ? "Authorize device" : "Create connection token"}</button>
273
+ </div>
274
+
275
+ <div id="result" class="hidden">
276
+ <p class="sub" id="resultMsg">Token created. Paste this into your agent's MCP config:</p>
277
+ <pre id="mcpJson"></pre>
278
+ <p class="sub">Or from a terminal:</p>
279
+ <pre id="cliLine"></pre>
280
+ </div>
281
+
282
+ <div class="tokens">
283
+ <h2>Your connections</h2>
284
+ <div id="tokenList"><div class="meta">Loading…</div></div>
285
+ </div>
286
+ </div>
287
+ <script>
288
+ (function () {
289
+ var BASE = ${JSON.stringify(joinAppPath(connectBasePath, "/_agent-native/mcp/connect"))};
290
+ var USER_CODE = ${JSON.stringify(safeUserCode || null)};
291
+ var msgEl = document.getElementById("msg");
292
+ function showMsg(text, kind) {
293
+ msgEl.textContent = text;
294
+ msgEl.className = "msg " + (kind || "err");
295
+ }
296
+ function clearMsg() { msgEl.className = "msg"; msgEl.textContent = ""; }
297
+
298
+ function renderResult(data) {
299
+ document.getElementById("mintForm").classList.add("hidden");
300
+ var entry = {};
301
+ entry[data.serverName] = data.mcpServerEntry;
302
+ document.getElementById("mcpJson").textContent =
303
+ JSON.stringify({ mcpServers: entry }, null, 2);
304
+ document.getElementById("cliLine").textContent = data.cli;
305
+ document.getElementById("result").classList.remove("hidden");
306
+ }
307
+
308
+ async function postJson(path, body) {
309
+ var res = await fetch(BASE + path, {
310
+ method: "POST",
311
+ headers: { "Content-Type": "application/json" },
312
+ credentials: "same-origin",
313
+ body: JSON.stringify(body || {})
314
+ });
315
+ var data = null;
316
+ try { data = await res.json(); } catch (e) {}
317
+ return { ok: res.ok, status: res.status, data: data };
318
+ }
319
+
320
+ async function loadTokens() {
321
+ var listEl = document.getElementById("tokenList");
322
+ try {
323
+ var res = await fetch(BASE + "/tokens", { credentials: "same-origin" });
324
+ if (!res.ok) { listEl.innerHTML = '<div class="meta">Could not load.</div>'; return; }
325
+ var data = await res.json();
326
+ var tokens = (data && data.tokens) || [];
327
+ if (!tokens.length) { listEl.innerHTML = '<div class="meta">No connections yet.</div>'; return; }
328
+ listEl.innerHTML = "";
329
+ tokens.forEach(function (t) {
330
+ var div = document.createElement("div");
331
+ div.className = "tok" + (t.revokedAt ? " revoked" : "");
332
+ var when = t.createdAt ? new Date(t.createdAt).toLocaleString() : "";
333
+ var used = t.lastUsedAt ? " · last used " + new Date(t.lastUsedAt).toLocaleString() : "";
334
+ var left = document.createElement("div");
335
+ var label = document.createElement("div");
336
+ label.textContent = t.label || "(unlabeled)";
337
+ var meta = document.createElement("div");
338
+ meta.className = "meta";
339
+ meta.textContent = (t.revokedAt ? "Revoked · " : "Created ") + when + used;
340
+ left.appendChild(label); left.appendChild(meta);
341
+ div.appendChild(left);
342
+ if (!t.revokedAt) {
343
+ var btn = document.createElement("button");
344
+ btn.className = "ghost";
345
+ btn.textContent = "Revoke";
346
+ btn.onclick = async function () {
347
+ btn.disabled = true;
348
+ var r = await postJson("/tokens/revoke", { id: t.id });
349
+ if (r.ok) { loadTokens(); }
350
+ else { btn.disabled = false; showMsg("Could not revoke token."); }
351
+ };
352
+ div.appendChild(btn);
353
+ }
354
+ listEl.appendChild(div);
355
+ });
356
+ } catch (e) {
357
+ listEl.innerHTML = '<div class="meta">Could not load.</div>';
358
+ }
359
+ }
360
+
361
+ document.getElementById("authorizeBtn").onclick = async function () {
362
+ var btn = this;
363
+ btn.disabled = true;
364
+ clearMsg();
365
+ var label = document.getElementById("label").value || undefined;
366
+ var ttlDays = parseInt(document.getElementById("ttl").value, 10) || undefined;
367
+ try {
368
+ if (USER_CODE) {
369
+ var a = await postJson("/device/authorize", { user_code: USER_CODE });
370
+ if (!a.ok) {
371
+ btn.disabled = false;
372
+ showMsg((a.data && a.data.error) || "Could not authorize this device code.");
373
+ return;
374
+ }
375
+ showMsg("Device authorized. You can return to your terminal — it will connect automatically.", "ok");
376
+ btn.classList.add("hidden");
377
+ document.getElementById("mintForm").classList.add("hidden");
378
+ } else {
379
+ var m = await postJson("/token", { label: label, ttlDays: ttlDays });
380
+ if (!m.ok) {
381
+ btn.disabled = false;
382
+ showMsg((m.data && m.data.error) || "Could not create token.");
383
+ return;
384
+ }
385
+ renderResult(m.data);
386
+ }
387
+ loadTokens();
388
+ } catch (e) {
389
+ btn.disabled = false;
390
+ showMsg("Network error. Please try again.");
391
+ }
392
+ };
393
+
394
+ loadTokens();
395
+ })();
396
+ </script>
397
+ </body>
398
+ </html>`;
399
+ }
400
+ // ---------------------------------------------------------------------------
401
+ // Handler — single entry point; core-routes-plugin dispatches the subpath.
402
+ // ---------------------------------------------------------------------------
403
+ /**
404
+ * Handle a `/_agent-native/mcp/connect[...]` request. `subpath` is the part
405
+ * after `/connect` (empty string = the page itself, otherwise e.g.
406
+ * `/token`, `/device/start`). The core-routes-plugin computes it from the
407
+ * stripped event path so this module stays mount-agnostic.
408
+ */
409
+ export async function handleMcpConnect(event, subpath, options = {}) {
410
+ const method = getMethod(event);
411
+ const origin = deriveOrigin(event);
412
+ const basePath = configuredBasePath();
413
+ const appUrl = `${origin}${basePath}`;
414
+ const sub = ("/" + subpath.replace(/^\/+/, "").replace(/\/+$/, "")).replace(/^\/$/, "");
415
+ // ---- The connect page (GET) ------------------------------------------
416
+ if (sub === "") {
417
+ if (method !== "GET" && method !== "HEAD") {
418
+ return json({ error: "Method not allowed" }, 405);
419
+ }
420
+ const session = await getSession(event);
421
+ if (!session?.email) {
422
+ // Serve the SAME login form the guard would, at this same URL — the
423
+ // login form reloads window.location so we re-enter here authed.
424
+ const loginHtml = getConfiguredLoginHtml(event);
425
+ if (loginHtml)
426
+ return html(loginHtml, 200);
427
+ // Fully-open app (no auth guard): nothing to scope a mint to.
428
+ return html(renderConnectPage({
429
+ origin: appUrl,
430
+ connectBasePath: basePath,
431
+ email: "(no auth configured)",
432
+ appName: options.appName || appLabel(appUrl, options),
433
+ userCode: null,
434
+ }));
435
+ }
436
+ let userCode = null;
437
+ try {
438
+ const u = new URL(event.node?.req?.url ?? event.path ?? "/", "http://an.invalid");
439
+ const raw = u.searchParams.get("user_code");
440
+ if (raw && USER_CODE_RE.test(raw))
441
+ userCode = raw;
442
+ }
443
+ catch {
444
+ userCode = null;
445
+ }
446
+ return html(renderConnectPage({
447
+ origin: appUrl,
448
+ connectBasePath: basePath,
449
+ email: session.email,
450
+ appName: options.appName || appLabel(appUrl, options),
451
+ userCode,
452
+ }));
453
+ }
454
+ // ---- POST /token (session-required) ---------------------------------
455
+ if (sub === "/token") {
456
+ if (method !== "POST")
457
+ return json({ error: "Method not allowed" }, 405);
458
+ const session = await getSession(event);
459
+ if (!session?.email)
460
+ return json({ error: "Unauthorized" }, 401);
461
+ if (!process.env.A2A_SECRET) {
462
+ return json({
463
+ error: "This deployment has no A2A_SECRET configured, so connect tokens cannot be minted.",
464
+ }, 503);
465
+ }
466
+ const body = ((await readBody(event).catch(() => ({}))) ?? {});
467
+ const label = typeof body.label === "string" && body.label.trim()
468
+ ? body.label.trim().slice(0, 120)
469
+ : null;
470
+ const ttlDays = clampTtlDays(body.ttlDays);
471
+ try {
472
+ const { token } = await mintConnectToken({
473
+ email: session.email,
474
+ orgId: session.orgId,
475
+ label,
476
+ ttlDays,
477
+ });
478
+ return json(mcpResultPayload(appUrl, token, options));
479
+ }
480
+ catch {
481
+ return json({ error: "Failed to mint token." }, 500);
482
+ }
483
+ }
484
+ // ---- POST /device/start (UNAUTH) ------------------------------------
485
+ if (sub === "/device/start") {
486
+ if (method !== "POST")
487
+ return json({ error: "Method not allowed" }, 405);
488
+ try {
489
+ const row = await createDeviceCode();
490
+ const verificationUri = `${appUrl}/_agent-native/mcp/connect`;
491
+ return json({
492
+ device_code: row.deviceCode,
493
+ user_code: row.userCode,
494
+ verification_uri: verificationUri,
495
+ verification_uri_complete: `${verificationUri}?user_code=${row.userCode}`,
496
+ interval: DEVICE_POLL_INTERVAL_S,
497
+ expires_in: Math.floor(DEVICE_CODE_TTL_MS / 1000),
498
+ });
499
+ }
500
+ catch (err) {
501
+ if (err?.message === "RATE_LIMITED") {
502
+ return json({ error: "Rate limited. Try again shortly." }, 429);
503
+ }
504
+ return json({ error: "Could not start device flow." }, 500);
505
+ }
506
+ }
507
+ // ---- POST /device/authorize (session-required) ----------------------
508
+ if (sub === "/device/authorize") {
509
+ if (method !== "POST")
510
+ return json({ error: "Method not allowed" }, 405);
511
+ const session = await getSession(event);
512
+ if (!session?.email)
513
+ return json({ error: "Unauthorized" }, 401);
514
+ const body = ((await readBody(event).catch(() => ({}))) ?? {});
515
+ const userCode = typeof body.user_code === "string" ? body.user_code.trim() : "";
516
+ if (!USER_CODE_RE.test(userCode)) {
517
+ return json({ error: "Invalid user code." }, 400);
518
+ }
519
+ const orgId = typeof session.orgId === "string" && session.orgId.trim()
520
+ ? session.orgId.trim()
521
+ : null;
522
+ const result = await approveDeviceCode(userCode, session.email, orgId);
523
+ if (result === "not_found") {
524
+ return json({ error: "Unknown device code." }, 404);
525
+ }
526
+ if (result === "expired") {
527
+ return json({ error: "This device code has expired." }, 410);
528
+ }
529
+ if (result === "already") {
530
+ return json({ error: "This device code was already used." }, 409);
531
+ }
532
+ return json({ status: "approved" });
533
+ }
534
+ // ---- POST /device/poll (UNAUTH) -------------------------------------
535
+ if (sub === "/device/poll") {
536
+ if (method !== "POST")
537
+ return json({ error: "Method not allowed" }, 405);
538
+ const body = ((await readBody(event).catch(() => ({}))) ?? {});
539
+ const deviceCode = typeof body.device_code === "string" ? body.device_code : "";
540
+ if (!deviceCode)
541
+ return json({ error: "device_code required" }, 400);
542
+ const row = await getDeviceCode(deviceCode);
543
+ if (!row)
544
+ return json({ status: "not_found" }, 404);
545
+ if (row.status === "consumed")
546
+ return json({ status: "consumed" });
547
+ if (row.status === "expired" ||
548
+ (row.expiresAt != null && row.expiresAt < Date.now())) {
549
+ if (row.status !== "expired")
550
+ void expireDeviceCode(deviceCode);
551
+ return json({ status: "expired" });
552
+ }
553
+ if (row.status === "pending" ||
554
+ row.status === "minting" ||
555
+ !row.ownerEmail) {
556
+ return json({ status: "pending" });
557
+ }
558
+ // status === "approved" && ownerEmail bound → mint exactly once.
559
+ if (!process.env.A2A_SECRET) {
560
+ return json({ status: "error", error: "A2A_SECRET not configured" }, 503);
561
+ }
562
+ try {
563
+ const jti = randomUUID();
564
+ // Claim a retryable minting state first. If signing or recording fails,
565
+ // release the row back to approved so the CLI can poll again.
566
+ const claimed = await claimDeviceCodeForMint(deviceCode, jti);
567
+ if (!claimed) {
568
+ const fresh = await getDeviceCode(deviceCode);
569
+ if (fresh?.status === "consumed")
570
+ return json({ status: "consumed" });
571
+ return json({ status: "pending" });
572
+ }
573
+ let token;
574
+ try {
575
+ const orgDomain = await resolveOrgDomain(claimed.orgId ?? undefined);
576
+ token = await signA2AToken(claimed.ownerEmail, orgDomain, undefined, {
577
+ preferGlobalSecret: true,
578
+ expiresIn: `${DEFAULT_TOKEN_TTL_DAYS}d`,
579
+ extraClaims: { jti, scope: MCP_CONNECT_SCOPE },
580
+ });
581
+ await recordMintedToken({
582
+ jti,
583
+ ownerEmail: claimed.ownerEmail,
584
+ orgId: claimed.orgId,
585
+ label: "Device connection",
586
+ });
587
+ if (!(await finishDeviceCodeMint(deviceCode, jti))) {
588
+ return json({ status: "pending" });
589
+ }
590
+ }
591
+ catch (err) {
592
+ await releaseDeviceCodeMint(deviceCode, jti);
593
+ throw err;
594
+ }
595
+ return json({
596
+ status: "approved",
597
+ ...mcpResultPayload(appUrl, token, options),
598
+ });
599
+ }
600
+ catch {
601
+ return json({ status: "error", error: "Failed to mint token." }, 500);
602
+ }
603
+ }
604
+ // ---- GET /tokens (session-required) ---------------------------------
605
+ if (sub === "/tokens") {
606
+ if (method !== "GET")
607
+ return json({ error: "Method not allowed" }, 405);
608
+ const session = await getSession(event);
609
+ if (!session?.email)
610
+ return json({ error: "Unauthorized" }, 401);
611
+ const rows = await listTokens(session.email);
612
+ return json({
613
+ tokens: rows.map((r) => ({
614
+ id: r.id,
615
+ label: r.label,
616
+ createdAt: r.createdAt,
617
+ lastUsedAt: r.lastUsedAt,
618
+ revokedAt: r.revokedAt,
619
+ })),
620
+ });
621
+ }
622
+ // ---- POST /tokens/revoke (session-required) -------------------------
623
+ if (sub === "/tokens/revoke") {
624
+ if (method !== "POST")
625
+ return json({ error: "Method not allowed" }, 405);
626
+ const session = await getSession(event);
627
+ if (!session?.email)
628
+ return json({ error: "Unauthorized" }, 401);
629
+ const body = ((await readBody(event).catch(() => ({}))) ?? {});
630
+ const id = typeof body.id === "string" ? body.id : "";
631
+ if (!id)
632
+ return json({ error: "id required" }, 400);
633
+ const revoked = await revokeToken(session.email, id);
634
+ return json({ ok: revoked });
635
+ }
636
+ return json({ error: "Not found" }, 404);
637
+ }
638
+ //# sourceMappingURL=connect-route.js.map