@agent-native/core 0.21.0 → 0.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/action-change-marker.d.ts +11 -0
- package/dist/action-change-marker.d.ts.map +1 -0
- package/dist/action-change-marker.js +52 -0
- package/dist/action-change-marker.js.map +1 -0
- package/dist/action.d.ts +2 -2
- package/dist/action.js +1 -1
- package/dist/action.js.map +1 -1
- package/dist/agent/production-agent.d.ts +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +4 -6
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/cli/connect.d.ts +5 -3
- package/dist/cli/connect.d.ts.map +1 -1
- package/dist/cli/connect.js +127 -15
- package/dist/cli/connect.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +6 -2
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +7 -1
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/NewWorkspaceAppFlow.js +1 -1
- package/dist/client/NewWorkspaceAppFlow.js.map +1 -1
- package/dist/client/agent-chat.d.ts.map +1 -1
- package/dist/client/agent-chat.js +13 -8
- package/dist/client/agent-chat.js.map +1 -1
- package/dist/client/agent-sidebar-state.d.ts +2 -0
- package/dist/client/agent-sidebar-state.d.ts.map +1 -1
- package/dist/client/agent-sidebar-state.js +40 -7
- package/dist/client/agent-sidebar-state.js.map +1 -1
- package/dist/client/mcp-apps/McpAppRenderer.d.ts.map +1 -1
- package/dist/client/mcp-apps/McpAppRenderer.js +9 -4
- package/dist/client/mcp-apps/McpAppRenderer.js.map +1 -1
- package/dist/client/use-db-sync.d.ts +5 -5
- package/dist/client/use-db-sync.d.ts.map +1 -1
- package/dist/client/use-db-sync.js +15 -5
- package/dist/client/use-db-sync.js.map +1 -1
- package/dist/client/use-db-sync.spec.d.ts +2 -0
- package/dist/client/use-db-sync.spec.d.ts.map +1 -0
- package/dist/client/use-db-sync.spec.js +80 -0
- package/dist/client/use-db-sync.spec.js.map +1 -0
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +14 -8
- package/dist/db/client.js.map +1 -1
- package/dist/extensions/actions.d.ts.map +1 -1
- package/dist/extensions/actions.js +62 -3
- package/dist/extensions/actions.js.map +1 -1
- package/dist/extensions/content-patch.d.ts +71 -0
- package/dist/extensions/content-patch.d.ts.map +1 -0
- package/dist/extensions/content-patch.js +251 -0
- package/dist/extensions/content-patch.js.map +1 -0
- package/dist/extensions/routes.js +6 -1
- package/dist/extensions/routes.js.map +1 -1
- package/dist/extensions/store.d.ts +4 -4
- package/dist/extensions/store.d.ts.map +1 -1
- package/dist/extensions/store.js +14 -18
- package/dist/extensions/store.js.map +1 -1
- package/dist/mcp/build-server.d.ts +3 -0
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +55 -6
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/oauth-route.d.ts +22 -0
- package/dist/mcp/oauth-route.d.ts.map +1 -0
- package/dist/mcp/oauth-route.js +618 -0
- package/dist/mcp/oauth-route.js.map +1 -0
- package/dist/mcp/oauth-store.d.ts +89 -0
- package/dist/mcp/oauth-store.d.ts.map +1 -0
- package/dist/mcp/oauth-store.js +391 -0
- package/dist/mcp/oauth-store.js.map +1 -0
- package/dist/mcp/oauth-token.d.ts +28 -0
- package/dist/mcp/oauth-token.d.ts.map +1 -0
- package/dist/mcp/oauth-token.js +83 -0
- package/dist/mcp/oauth-token.js.map +1 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +5 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp-client/index.d.ts.map +1 -1
- package/dist/mcp-client/index.js +16 -2
- package/dist/mcp-client/index.js.map +1 -1
- package/dist/mcp-client/routes.js +18 -5
- package/dist/mcp-client/routes.js.map +1 -1
- package/dist/scripts/dev/shell.d.ts.map +1 -1
- package/dist/scripts/dev/shell.js +24 -1
- package/dist/scripts/dev/shell.js.map +1 -1
- package/dist/scripts/runner.d.ts.map +1 -1
- package/dist/scripts/runner.js +7 -0
- package/dist/scripts/runner.js.map +1 -1
- package/dist/server/action-change.d.ts +8 -0
- package/dist/server/action-change.d.ts.map +1 -0
- package/dist/server/action-change.js +38 -0
- package/dist/server/action-change.js.map +1 -0
- package/dist/server/action-routes.d.ts.map +1 -1
- package/dist/server/action-routes.js +4 -6
- package/dist/server/action-routes.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +3 -2
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +14 -8
- package/dist/server/auth.js.map +1 -1
- package/dist/server/builder-browser.d.ts +6 -0
- package/dist/server/builder-browser.d.ts.map +1 -1
- package/dist/server/builder-browser.js +15 -0
- package/dist/server/builder-browser.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +5 -4
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +17 -2
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/poll.d.ts.map +1 -1
- package/dist/server/poll.js +55 -3
- package/dist/server/poll.js.map +1 -1
- package/dist/templates/default/.agents/skills/actions/SKILL.md +193 -72
- package/dist/templates/default/.agents/skills/real-time-sync/SKILL.md +88 -38
- package/dist/templates/default/AGENTS.md +3 -3
- package/dist/templates/default/actions/hello.ts +13 -20
- package/dist/templates/default/actions/navigate.ts +19 -51
- package/dist/templates/default/actions/view-screen.ts +16 -33
- package/dist/templates/default/app/hooks/use-navigation-state.ts +13 -3
- package/dist/templates/default/app/lib/tab-id.ts +1 -0
- package/dist/templates/default/app/root.tsx +2 -1
- package/dist/templates/default/app/routes/_index.tsx +11 -0
- package/dist/templates/default/package.json +2 -1
- package/dist/templates/workspace-core/.agents/skills/actions/SKILL.md +1 -1
- package/dist/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +9 -1
- package/dist/templates/workspace-core/AGENTS.md +8 -0
- package/dist/templates/workspace-root/AGENTS.md +7 -0
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +2 -2
- package/dist/vite/client.js.map +1 -1
- package/docs/content/actions.md +1 -1
- package/docs/content/authentication.md +16 -1
- package/docs/content/client.md +11 -8
- package/docs/content/context-awareness.md +2 -3
- package/docs/content/creating-templates.md +2 -2
- package/docs/content/external-agents.md +48 -15
- package/docs/content/faq.md +2 -2
- package/docs/content/key-concepts.md +31 -23
- package/docs/content/mcp-protocol.md +50 -17
- package/docs/content/template-starter.md +3 -3
- package/docs/content/what-is-agent-native.md +5 -3
- package/package.json +2 -1
- package/src/templates/default/.agents/skills/actions/SKILL.md +193 -72
- package/src/templates/default/.agents/skills/real-time-sync/SKILL.md +88 -38
- package/src/templates/default/AGENTS.md +3 -3
- package/src/templates/default/actions/hello.ts +13 -20
- package/src/templates/default/actions/navigate.ts +19 -51
- package/src/templates/default/actions/view-screen.ts +16 -33
- package/src/templates/default/app/hooks/use-navigation-state.ts +13 -3
- package/src/templates/default/app/lib/tab-id.ts +1 -0
- package/src/templates/default/app/root.tsx +2 -1
- package/src/templates/default/app/routes/_index.tsx +11 -0
- package/src/templates/default/package.json +2 -1
- package/src/templates/workspace-core/.agents/skills/actions/SKILL.md +1 -1
- package/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +9 -1
- package/src/templates/workspace-core/AGENTS.md +8 -0
- package/src/templates/workspace-root/AGENTS.md +7 -0
- package/dist/templates/default/server/routes/api/hello.get.ts +0 -5
- package/dist/templates/default/shared/api.ts +0 -6
- package/src/templates/default/server/routes/api/hello.get.ts +0 -5
- package/src/templates/default/shared/api.ts +0 -6
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standard remote MCP OAuth 2.1 endpoints.
|
|
3
|
+
*
|
|
4
|
+
* These routes let MCP hosts such as Claude Code and ChatGPT authenticate
|
|
5
|
+
* through their native remote-MCP OAuth flow instead of pasting bearer tokens.
|
|
6
|
+
* The issued access tokens are audience-bound to `/_agent-native/mcp`, carry
|
|
7
|
+
* the same user/org identity as the existing connect flow, and are mediated by
|
|
8
|
+
* `verifyAuth` before any MCP tool/resource request runs.
|
|
9
|
+
*/
|
|
10
|
+
import type { H3Event } from "h3";
|
|
11
|
+
export interface McpOAuthRouteOptions {
|
|
12
|
+
appId?: string;
|
|
13
|
+
appName?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function getMcpOAuthIssuer(event: H3Event): string | undefined;
|
|
16
|
+
export declare function getMcpOAuthResource(event: H3Event): string | undefined;
|
|
17
|
+
export declare function getMcpOAuthProtectedResourceMetadataUrl(event: H3Event): string | undefined;
|
|
18
|
+
export declare function buildMcpOAuthChallenge(event: H3Event): string;
|
|
19
|
+
export declare function handleMcpOAuthProtectedResourceMetadata(event: H3Event): Response;
|
|
20
|
+
export declare function handleMcpOAuthAuthorizationServerMetadata(event: H3Event): Response;
|
|
21
|
+
export declare function handleMcpOAuth(event: H3Event, subpath: string, options?: McpOAuthRouteOptions): Promise<Response>;
|
|
22
|
+
//# sourceMappingURL=oauth-route.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-route.d.ts","sourceRoot":"","sources":["../../src/mcp/oauth-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAyBlC,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAkFD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAIpE;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAItE;AAED,wBAAgB,uCAAuC,CACrD,KAAK,EAAE,OAAO,GACb,MAAM,GAAG,SAAS,CAIpB;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAM7D;AAiBD,wBAAgB,uCAAuC,CACrD,KAAK,EAAE,OAAO,GACb,QAAQ,CAeV;AAED,wBAAgB,yCAAyC,CACvD,KAAK,EAAE,OAAO,GACb,QAAQ,CAsBV;AA2jBD,wBAAsB,cAAc,CAClC,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,QAAQ,CAAC,CAenB"}
|
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standard remote MCP OAuth 2.1 endpoints.
|
|
3
|
+
*
|
|
4
|
+
* These routes let MCP hosts such as Claude Code and ChatGPT authenticate
|
|
5
|
+
* through their native remote-MCP OAuth flow instead of pasting bearer tokens.
|
|
6
|
+
* The issued access tokens are audience-bound to `/_agent-native/mcp`, carry
|
|
7
|
+
* the same user/org identity as the existing connect flow, and are mediated by
|
|
8
|
+
* `verifyAuth` before any MCP tool/resource request runs.
|
|
9
|
+
*/
|
|
10
|
+
import { getHeader, getMethod, getQuery, setResponseStatus } from "h3";
|
|
11
|
+
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
|
|
12
|
+
import { readBody } from "../server/h3-helpers.js";
|
|
13
|
+
import { getConfiguredLoginHtml, getSession } from "../server/auth.js";
|
|
14
|
+
import { getAuthSecret } from "../server/better-auth-instance.js";
|
|
15
|
+
import { getOrgDomain } from "../org/context.js";
|
|
16
|
+
import { createOAuthCode, createOAuthRefreshToken, consumeOAuthCode, generateOpaqueToken, getOAuthClient, getOAuthCode, getOAuthRefreshToken, registerOAuthClient, rotateOAuthRefreshToken, } from "./oauth-store.js";
|
|
17
|
+
import { MCP_OAUTH_DEFAULT_SCOPE, MCP_OAUTH_SCOPES, normalizeOAuthScope, signMcpOAuthAccessToken, } from "./oauth-token.js";
|
|
18
|
+
function json(body, status = 200) {
|
|
19
|
+
return new Response(JSON.stringify(body), {
|
|
20
|
+
status,
|
|
21
|
+
headers: {
|
|
22
|
+
"Content-Type": "application/json",
|
|
23
|
+
"Cache-Control": "no-store",
|
|
24
|
+
Pragma: "no-cache",
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
function html(body, status = 200) {
|
|
29
|
+
return new Response(body, {
|
|
30
|
+
status,
|
|
31
|
+
headers: {
|
|
32
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
33
|
+
"Cache-Control": "no-store",
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function redirect(location) {
|
|
38
|
+
return new Response(null, {
|
|
39
|
+
status: 302,
|
|
40
|
+
headers: { Location: location, "Cache-Control": "no-store" },
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function isSameOriginPost(event) {
|
|
44
|
+
const origin = getHeader(event, "origin");
|
|
45
|
+
if (!origin)
|
|
46
|
+
return true;
|
|
47
|
+
const issuer = getMcpOAuthIssuer(event);
|
|
48
|
+
if (!issuer)
|
|
49
|
+
return false;
|
|
50
|
+
try {
|
|
51
|
+
return new URL(origin).origin === new URL(issuer).origin;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function oauthError(error, description, status = 400) {
|
|
58
|
+
return json({ error, error_description: description }, status);
|
|
59
|
+
}
|
|
60
|
+
function escapeHtml(s) {
|
|
61
|
+
return s
|
|
62
|
+
.replace(/&/g, "&")
|
|
63
|
+
.replace(/</g, "<")
|
|
64
|
+
.replace(/>/g, ">")
|
|
65
|
+
.replace(/"/g, """);
|
|
66
|
+
}
|
|
67
|
+
function normalizeBasePath(raw) {
|
|
68
|
+
const trimmed = (raw ?? "").trim();
|
|
69
|
+
if (!trimmed || trimmed === "/")
|
|
70
|
+
return "";
|
|
71
|
+
const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
72
|
+
return withSlash.replace(/\/+$/, "");
|
|
73
|
+
}
|
|
74
|
+
function configuredBasePath() {
|
|
75
|
+
return normalizeBasePath(process.env.APP_BASE_PATH || process.env.VITE_APP_BASE_PATH);
|
|
76
|
+
}
|
|
77
|
+
function deriveOrigin(event) {
|
|
78
|
+
const forwardedProto = getHeader(event, "x-forwarded-proto");
|
|
79
|
+
const host = getHeader(event, "x-forwarded-host") || getHeader(event, "host");
|
|
80
|
+
const proto = forwardedProto?.split(",")[0]?.trim() ||
|
|
81
|
+
(host && /^(localhost|127\.0\.0\.1|\[::1\])(:|$)/.test(host)
|
|
82
|
+
? "http"
|
|
83
|
+
: "https");
|
|
84
|
+
return host ? `${proto}://${host}` : "";
|
|
85
|
+
}
|
|
86
|
+
export function getMcpOAuthIssuer(event) {
|
|
87
|
+
const origin = deriveOrigin(event);
|
|
88
|
+
if (!origin)
|
|
89
|
+
return undefined;
|
|
90
|
+
return `${origin}${configuredBasePath()}`;
|
|
91
|
+
}
|
|
92
|
+
export function getMcpOAuthResource(event) {
|
|
93
|
+
const issuer = getMcpOAuthIssuer(event);
|
|
94
|
+
if (!issuer)
|
|
95
|
+
return undefined;
|
|
96
|
+
return `${issuer}/_agent-native/mcp`;
|
|
97
|
+
}
|
|
98
|
+
export function getMcpOAuthProtectedResourceMetadataUrl(event) {
|
|
99
|
+
const issuer = getMcpOAuthIssuer(event);
|
|
100
|
+
if (!issuer)
|
|
101
|
+
return undefined;
|
|
102
|
+
return `${issuer}/.well-known/oauth-protected-resource`;
|
|
103
|
+
}
|
|
104
|
+
export function buildMcpOAuthChallenge(event) {
|
|
105
|
+
const metadata = getMcpOAuthProtectedResourceMetadataUrl(event);
|
|
106
|
+
const scope = MCP_OAUTH_DEFAULT_SCOPE;
|
|
107
|
+
return metadata
|
|
108
|
+
? `Bearer resource_metadata="${metadata}", scope="${scope}"`
|
|
109
|
+
: `Bearer scope="${scope}"`;
|
|
110
|
+
}
|
|
111
|
+
function authorizationEndpoint(event) {
|
|
112
|
+
const issuer = getMcpOAuthIssuer(event);
|
|
113
|
+
return issuer ? `${issuer}/_agent-native/mcp/oauth/authorize` : undefined;
|
|
114
|
+
}
|
|
115
|
+
function tokenEndpoint(event) {
|
|
116
|
+
const issuer = getMcpOAuthIssuer(event);
|
|
117
|
+
return issuer ? `${issuer}/_agent-native/mcp/oauth/token` : undefined;
|
|
118
|
+
}
|
|
119
|
+
function registrationEndpoint(event) {
|
|
120
|
+
const issuer = getMcpOAuthIssuer(event);
|
|
121
|
+
return issuer ? `${issuer}/_agent-native/mcp/oauth/register` : undefined;
|
|
122
|
+
}
|
|
123
|
+
export function handleMcpOAuthProtectedResourceMetadata(event) {
|
|
124
|
+
if (getMethod(event) !== "GET") {
|
|
125
|
+
return oauthError("invalid_request", "Method not allowed", 405);
|
|
126
|
+
}
|
|
127
|
+
const resource = getMcpOAuthResource(event);
|
|
128
|
+
const issuer = getMcpOAuthIssuer(event);
|
|
129
|
+
if (!resource || !issuer) {
|
|
130
|
+
return oauthError("server_error", "Unable to derive MCP resource", 500);
|
|
131
|
+
}
|
|
132
|
+
return json({
|
|
133
|
+
resource,
|
|
134
|
+
authorization_servers: [issuer],
|
|
135
|
+
scopes_supported: MCP_OAUTH_SCOPES,
|
|
136
|
+
resource_documentation: issuer,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
export function handleMcpOAuthAuthorizationServerMetadata(event) {
|
|
140
|
+
if (getMethod(event) !== "GET") {
|
|
141
|
+
return oauthError("invalid_request", "Method not allowed", 405);
|
|
142
|
+
}
|
|
143
|
+
const issuer = getMcpOAuthIssuer(event);
|
|
144
|
+
const authorize = authorizationEndpoint(event);
|
|
145
|
+
const token = tokenEndpoint(event);
|
|
146
|
+
const register = registrationEndpoint(event);
|
|
147
|
+
if (!issuer || !authorize || !token || !register) {
|
|
148
|
+
return oauthError("server_error", "Unable to derive OAuth endpoints", 500);
|
|
149
|
+
}
|
|
150
|
+
return json({
|
|
151
|
+
issuer,
|
|
152
|
+
authorization_endpoint: authorize,
|
|
153
|
+
token_endpoint: token,
|
|
154
|
+
registration_endpoint: register,
|
|
155
|
+
response_types_supported: ["code"],
|
|
156
|
+
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
157
|
+
code_challenge_methods_supported: ["S256"],
|
|
158
|
+
token_endpoint_auth_methods_supported: ["none"],
|
|
159
|
+
scopes_supported: MCP_OAUTH_SCOPES,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function isAllowedRedirectUri(value) {
|
|
163
|
+
if (typeof value !== "string" || value.length > 2048)
|
|
164
|
+
return false;
|
|
165
|
+
try {
|
|
166
|
+
const url = new URL(value);
|
|
167
|
+
if (url.hash)
|
|
168
|
+
return false;
|
|
169
|
+
if (url.username || url.password)
|
|
170
|
+
return false;
|
|
171
|
+
if (url.protocol === "https:")
|
|
172
|
+
return true;
|
|
173
|
+
if (url.protocol !== "http:")
|
|
174
|
+
return false;
|
|
175
|
+
return (url.hostname === "localhost" ||
|
|
176
|
+
url.hostname === "127.0.0.1" ||
|
|
177
|
+
url.hostname === "::1" ||
|
|
178
|
+
url.hostname === "[::1]");
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function parseStringArray(value) {
|
|
185
|
+
return Array.isArray(value)
|
|
186
|
+
? value.filter((item) => typeof item === "string")
|
|
187
|
+
: [];
|
|
188
|
+
}
|
|
189
|
+
async function handleRegister(event) {
|
|
190
|
+
if (getMethod(event) !== "POST") {
|
|
191
|
+
return oauthError("invalid_request", "Method not allowed", 405);
|
|
192
|
+
}
|
|
193
|
+
const body = ((await readBody(event).catch(() => ({}))) ?? {});
|
|
194
|
+
const redirectUris = parseStringArray(body.redirect_uris);
|
|
195
|
+
if (redirectUris.length === 0 ||
|
|
196
|
+
redirectUris.length > 20 ||
|
|
197
|
+
!redirectUris.every(isAllowedRedirectUri)) {
|
|
198
|
+
return oauthError("invalid_client_metadata", "redirect_uris must contain valid HTTPS or localhost callback URLs");
|
|
199
|
+
}
|
|
200
|
+
const grantTypes = parseStringArray(body.grant_types);
|
|
201
|
+
if (grantTypes.length &&
|
|
202
|
+
!grantTypes.every((g) => g === "authorization_code" || g === "refresh_token")) {
|
|
203
|
+
return oauthError("invalid_client_metadata", "Unsupported grant_type");
|
|
204
|
+
}
|
|
205
|
+
const responseTypes = parseStringArray(body.response_types);
|
|
206
|
+
if (responseTypes.length && !responseTypes.every((r) => r === "code")) {
|
|
207
|
+
return oauthError("invalid_client_metadata", "Unsupported response_type");
|
|
208
|
+
}
|
|
209
|
+
const method = typeof body.token_endpoint_auth_method === "string"
|
|
210
|
+
? body.token_endpoint_auth_method
|
|
211
|
+
: "none";
|
|
212
|
+
if (method !== "none") {
|
|
213
|
+
return oauthError("invalid_client_metadata", "Only public OAuth clients are supported");
|
|
214
|
+
}
|
|
215
|
+
const clientName = typeof body.client_name === "string"
|
|
216
|
+
? body.client_name.trim().slice(0, 120)
|
|
217
|
+
: null;
|
|
218
|
+
let client;
|
|
219
|
+
try {
|
|
220
|
+
client = await registerOAuthClient({
|
|
221
|
+
clientName,
|
|
222
|
+
redirectUris: [...new Set(redirectUris)],
|
|
223
|
+
grantTypes: grantTypes.length ? grantTypes : undefined,
|
|
224
|
+
responseTypes: responseTypes.length ? responseTypes : undefined,
|
|
225
|
+
tokenEndpointAuthMethod: method,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
if (err?.message === "RATE_LIMITED") {
|
|
230
|
+
return oauthError("slow_down", "Too many client registrations", 429);
|
|
231
|
+
}
|
|
232
|
+
throw err;
|
|
233
|
+
}
|
|
234
|
+
return json({
|
|
235
|
+
client_id: client.clientId,
|
|
236
|
+
client_id_issued_at: Math.floor((client.createdAt ?? Date.now()) / 1000),
|
|
237
|
+
client_name: client.clientName ?? undefined,
|
|
238
|
+
redirect_uris: client.redirectUris,
|
|
239
|
+
grant_types: client.grantTypes,
|
|
240
|
+
response_types: client.responseTypes,
|
|
241
|
+
token_endpoint_auth_method: client.tokenEndpointAuthMethod,
|
|
242
|
+
}, 201);
|
|
243
|
+
}
|
|
244
|
+
function redirectWithOAuthError(params) {
|
|
245
|
+
const url = new URL(params.redirectUri);
|
|
246
|
+
url.searchParams.set("error", params.error);
|
|
247
|
+
if (params.description) {
|
|
248
|
+
url.searchParams.set("error_description", params.description);
|
|
249
|
+
}
|
|
250
|
+
if (params.state)
|
|
251
|
+
url.searchParams.set("state", params.state);
|
|
252
|
+
return redirect(url.toString());
|
|
253
|
+
}
|
|
254
|
+
function redirectWithCode(params) {
|
|
255
|
+
const url = new URL(params.redirectUri);
|
|
256
|
+
url.searchParams.set("code", params.code);
|
|
257
|
+
if (params.state)
|
|
258
|
+
url.searchParams.set("state", params.state);
|
|
259
|
+
return redirect(url.toString());
|
|
260
|
+
}
|
|
261
|
+
function codeChallengeForVerifier(verifier) {
|
|
262
|
+
return createHash("sha256").update(verifier).digest("base64url");
|
|
263
|
+
}
|
|
264
|
+
function safeEqual(a, b) {
|
|
265
|
+
const aa = Buffer.from(a);
|
|
266
|
+
const bb = Buffer.from(b);
|
|
267
|
+
return aa.length === bb.length && timingSafeEqual(aa, bb);
|
|
268
|
+
}
|
|
269
|
+
function base64UrlEncode(value) {
|
|
270
|
+
const buf = typeof value === "string" ? Buffer.from(value, "utf8") : value;
|
|
271
|
+
return buf.toString("base64url");
|
|
272
|
+
}
|
|
273
|
+
function base64UrlDecode(value) {
|
|
274
|
+
return Buffer.from(value, "base64url");
|
|
275
|
+
}
|
|
276
|
+
function consentSigningKey() {
|
|
277
|
+
return process.env.A2A_SECRET || getAuthSecret();
|
|
278
|
+
}
|
|
279
|
+
function consentPayload(params) {
|
|
280
|
+
return JSON.stringify({
|
|
281
|
+
...params,
|
|
282
|
+
exp: Math.floor(Date.now() / 1000) + 10 * 60,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
function signConsentToken(params) {
|
|
286
|
+
const payload = base64UrlEncode(consentPayload(params));
|
|
287
|
+
const sig = base64UrlEncode(createHmac("sha256", consentSigningKey()).update(payload).digest());
|
|
288
|
+
return `${payload}.${sig}`;
|
|
289
|
+
}
|
|
290
|
+
function verifyConsentToken(token, expected) {
|
|
291
|
+
if (!token || !token.includes("."))
|
|
292
|
+
return false;
|
|
293
|
+
const [payload, sig] = token.split(".", 2);
|
|
294
|
+
if (!payload || !sig)
|
|
295
|
+
return false;
|
|
296
|
+
const expectedSig = base64UrlEncode(createHmac("sha256", consentSigningKey()).update(payload).digest());
|
|
297
|
+
if (!safeEqual(sig, expectedSig))
|
|
298
|
+
return false;
|
|
299
|
+
try {
|
|
300
|
+
const parsed = JSON.parse(base64UrlDecode(payload).toString("utf8"));
|
|
301
|
+
return (parsed.email === expected.email &&
|
|
302
|
+
parsed.clientId === expected.clientId &&
|
|
303
|
+
parsed.redirectUri === expected.redirectUri &&
|
|
304
|
+
parsed.resource === expected.resource &&
|
|
305
|
+
parsed.scope === expected.scope &&
|
|
306
|
+
parsed.codeChallenge === expected.codeChallenge &&
|
|
307
|
+
typeof parsed.exp === "number" &&
|
|
308
|
+
parsed.exp * 1000 >= Date.now());
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function isValidCodeVerifier(value) {
|
|
315
|
+
return (typeof value === "string" &&
|
|
316
|
+
value.length >= 43 &&
|
|
317
|
+
value.length <= 128 &&
|
|
318
|
+
/^[A-Za-z0-9._~-]+$/.test(value));
|
|
319
|
+
}
|
|
320
|
+
function renderConsentPage(params) {
|
|
321
|
+
const hidden = Object.entries(params.fields)
|
|
322
|
+
.map(([key, value]) => `<input type="hidden" name="${escapeHtml(key)}" value="${escapeHtml(value)}">`)
|
|
323
|
+
.join("\n");
|
|
324
|
+
const scopes = params.scopes
|
|
325
|
+
.map((scope) => `<li><code>${escapeHtml(scope)}</code></li>`)
|
|
326
|
+
.join("");
|
|
327
|
+
return `<!doctype html>
|
|
328
|
+
<html lang="en">
|
|
329
|
+
<head>
|
|
330
|
+
<meta charset="utf-8">
|
|
331
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
332
|
+
<title>Authorize ${escapeHtml(params.appName)}</title>
|
|
333
|
+
<style>
|
|
334
|
+
:root { color-scheme: dark; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #09090b; color: #f4f4f5; }
|
|
335
|
+
body { min-height: 100vh; display: grid; place-items: center; margin: 0; padding: 24px; }
|
|
336
|
+
main { width: min(520px, 100%); border: 1px solid #27272a; border-radius: 8px; background: #111113; padding: 24px; box-shadow: 0 24px 80px rgba(0,0,0,.35); }
|
|
337
|
+
h1 { font-size: 22px; line-height: 1.2; margin: 0 0 10px; }
|
|
338
|
+
p { color: #a1a1aa; line-height: 1.5; margin: 0 0 18px; }
|
|
339
|
+
ul { margin: 0 0 22px; padding-left: 22px; color: #d4d4d8; }
|
|
340
|
+
code { color: #67e8f9; }
|
|
341
|
+
.actions { display: flex; gap: 10px; justify-content: flex-end; }
|
|
342
|
+
button { border: 0; border-radius: 6px; padding: 10px 14px; font-weight: 650; cursor: pointer; }
|
|
343
|
+
.primary { background: #f4f4f5; color: #09090b; }
|
|
344
|
+
.secondary { background: #27272a; color: #f4f4f5; }
|
|
345
|
+
</style>
|
|
346
|
+
</head>
|
|
347
|
+
<body>
|
|
348
|
+
<main>
|
|
349
|
+
<h1>Authorize ${escapeHtml(params.clientName)}</h1>
|
|
350
|
+
<p>${escapeHtml(params.appName)} will let this MCP client act as ${escapeHtml(params.email)} for these scopes:</p>
|
|
351
|
+
<ul>${scopes}</ul>
|
|
352
|
+
<form method="post">
|
|
353
|
+
${hidden}
|
|
354
|
+
<div class="actions">
|
|
355
|
+
<button class="secondary" type="submit" name="decision" value="deny">Deny</button>
|
|
356
|
+
<button class="primary" type="submit" name="decision" value="approve">Authorize</button>
|
|
357
|
+
</div>
|
|
358
|
+
</form>
|
|
359
|
+
</main>
|
|
360
|
+
</body>
|
|
361
|
+
</html>`;
|
|
362
|
+
}
|
|
363
|
+
async function resolveOrgDomain(orgId) {
|
|
364
|
+
if (!orgId)
|
|
365
|
+
return undefined;
|
|
366
|
+
try {
|
|
367
|
+
return (await getOrgDomain(orgId)) ?? undefined;
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
return undefined;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
async function readOAuthParams(event) {
|
|
374
|
+
if (getMethod(event) === "GET") {
|
|
375
|
+
const query = getQuery(event);
|
|
376
|
+
return Object.fromEntries(Object.entries(query).flatMap(([key, value]) => typeof value === "string" ? [[key, value]] : []));
|
|
377
|
+
}
|
|
378
|
+
const body = await readBody(event).catch(() => ({}));
|
|
379
|
+
if (typeof body === "string") {
|
|
380
|
+
return Object.fromEntries(new URLSearchParams(body));
|
|
381
|
+
}
|
|
382
|
+
if (body && typeof body === "object") {
|
|
383
|
+
return Object.fromEntries(Object.entries(body).flatMap(([key, value]) => typeof value === "string" ? [[key, value]] : []));
|
|
384
|
+
}
|
|
385
|
+
return {};
|
|
386
|
+
}
|
|
387
|
+
async function handleAuthorize(event, options) {
|
|
388
|
+
const method = getMethod(event);
|
|
389
|
+
if (method !== "GET" && method !== "POST") {
|
|
390
|
+
return oauthError("invalid_request", "Method not allowed", 405);
|
|
391
|
+
}
|
|
392
|
+
if (method === "POST" && !isSameOriginPost(event)) {
|
|
393
|
+
return oauthError("invalid_request", "Cross-origin authorize POST rejected", 403);
|
|
394
|
+
}
|
|
395
|
+
const params = await readOAuthParams(event);
|
|
396
|
+
const state = params.state;
|
|
397
|
+
const clientId = params.client_id;
|
|
398
|
+
const redirectUri = params.redirect_uri;
|
|
399
|
+
const resource = params.resource || getMcpOAuthResource(event);
|
|
400
|
+
const expectedResource = getMcpOAuthResource(event);
|
|
401
|
+
if (params.response_type !== "code") {
|
|
402
|
+
return oauthError("unsupported_response_type", "response_type must be code");
|
|
403
|
+
}
|
|
404
|
+
if (!clientId || !redirectUri || !resource || resource !== expectedResource) {
|
|
405
|
+
return oauthError("invalid_request", "Invalid OAuth authorization request");
|
|
406
|
+
}
|
|
407
|
+
if (params.code_challenge_method !== "S256" || !params.code_challenge) {
|
|
408
|
+
return oauthError("invalid_request", "PKCE S256 is required");
|
|
409
|
+
}
|
|
410
|
+
const client = await getOAuthClient(clientId);
|
|
411
|
+
if (!client || !client.redirectUris.includes(redirectUri)) {
|
|
412
|
+
return oauthError("invalid_client", "Unknown client or redirect_uri");
|
|
413
|
+
}
|
|
414
|
+
const session = await getSession(event);
|
|
415
|
+
if (!session?.email) {
|
|
416
|
+
if (params.prompt === "none") {
|
|
417
|
+
return redirectWithOAuthError({
|
|
418
|
+
redirectUri,
|
|
419
|
+
state,
|
|
420
|
+
error: "login_required",
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
const loginHtml = getConfiguredLoginHtml(event);
|
|
424
|
+
return loginHtml
|
|
425
|
+
? html(loginHtml, 200)
|
|
426
|
+
: oauthError("login_required", "Sign in required", 401);
|
|
427
|
+
}
|
|
428
|
+
const scope = normalizeOAuthScope(params.scope);
|
|
429
|
+
if (!scope) {
|
|
430
|
+
return redirectWithOAuthError({
|
|
431
|
+
redirectUri,
|
|
432
|
+
state,
|
|
433
|
+
error: "invalid_scope",
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
if (method === "GET") {
|
|
437
|
+
return html(renderConsentPage({
|
|
438
|
+
appName: options.appName || options.appId || "Agent Native",
|
|
439
|
+
email: session.email,
|
|
440
|
+
clientName: client.clientName || client.clientId,
|
|
441
|
+
scopes: scope.split(/\s+/),
|
|
442
|
+
fields: {
|
|
443
|
+
response_type: "code",
|
|
444
|
+
client_id: clientId,
|
|
445
|
+
redirect_uri: redirectUri,
|
|
446
|
+
resource,
|
|
447
|
+
scope,
|
|
448
|
+
state: state ?? "",
|
|
449
|
+
code_challenge: params.code_challenge,
|
|
450
|
+
code_challenge_method: "S256",
|
|
451
|
+
consent_token: signConsentToken({
|
|
452
|
+
email: session.email,
|
|
453
|
+
clientId,
|
|
454
|
+
redirectUri,
|
|
455
|
+
resource,
|
|
456
|
+
scope,
|
|
457
|
+
codeChallenge: params.code_challenge,
|
|
458
|
+
}),
|
|
459
|
+
},
|
|
460
|
+
}));
|
|
461
|
+
}
|
|
462
|
+
if (!verifyConsentToken(params.consent_token, {
|
|
463
|
+
email: session.email,
|
|
464
|
+
clientId,
|
|
465
|
+
redirectUri,
|
|
466
|
+
resource,
|
|
467
|
+
scope,
|
|
468
|
+
codeChallenge: params.code_challenge,
|
|
469
|
+
})) {
|
|
470
|
+
return oauthError("invalid_request", "Invalid authorization consent token");
|
|
471
|
+
}
|
|
472
|
+
if (params.decision !== "approve") {
|
|
473
|
+
return redirectWithOAuthError({
|
|
474
|
+
redirectUri,
|
|
475
|
+
state,
|
|
476
|
+
error: "access_denied",
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
const orgDomain = await resolveOrgDomain(session.orgId);
|
|
480
|
+
const code = await createOAuthCode({
|
|
481
|
+
clientId,
|
|
482
|
+
redirectUri,
|
|
483
|
+
codeChallenge: params.code_challenge,
|
|
484
|
+
codeChallengeMethod: "S256",
|
|
485
|
+
ownerEmail: session.email,
|
|
486
|
+
orgId: session.orgId ?? null,
|
|
487
|
+
orgDomain: orgDomain ?? null,
|
|
488
|
+
scope,
|
|
489
|
+
resource,
|
|
490
|
+
});
|
|
491
|
+
return redirectWithCode({ redirectUri, state, code: code.code });
|
|
492
|
+
}
|
|
493
|
+
async function issueTokenSet(params) {
|
|
494
|
+
const refreshToken = generateOpaqueToken();
|
|
495
|
+
await createOAuthRefreshToken({
|
|
496
|
+
refreshToken,
|
|
497
|
+
clientId: params.clientId,
|
|
498
|
+
ownerEmail: params.ownerEmail,
|
|
499
|
+
orgId: params.orgId ?? null,
|
|
500
|
+
orgDomain: params.orgDomain ?? null,
|
|
501
|
+
scope: params.scope,
|
|
502
|
+
resource: params.resource,
|
|
503
|
+
});
|
|
504
|
+
const accessToken = await signMcpOAuthAccessToken(params);
|
|
505
|
+
return {
|
|
506
|
+
access_token: accessToken,
|
|
507
|
+
token_type: "Bearer",
|
|
508
|
+
expires_in: 3600,
|
|
509
|
+
refresh_token: refreshToken,
|
|
510
|
+
scope: params.scope,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
async function handleAuthorizationCodeGrant(event, body) {
|
|
514
|
+
const code = body.code;
|
|
515
|
+
const clientId = body.client_id;
|
|
516
|
+
const redirectUri = body.redirect_uri;
|
|
517
|
+
const verifier = body.code_verifier;
|
|
518
|
+
if (!code || !clientId || !redirectUri || !isValidCodeVerifier(verifier)) {
|
|
519
|
+
return oauthError("invalid_request", "Missing authorization-code fields");
|
|
520
|
+
}
|
|
521
|
+
const row = await getOAuthCode(code);
|
|
522
|
+
if (!row)
|
|
523
|
+
return oauthError("invalid_grant", "Invalid or expired code");
|
|
524
|
+
if (row.clientId !== clientId || row.redirectUri !== redirectUri) {
|
|
525
|
+
return oauthError("invalid_grant", "Code was issued to another client");
|
|
526
|
+
}
|
|
527
|
+
const expectedChallenge = codeChallengeForVerifier(verifier);
|
|
528
|
+
if (!safeEqual(expectedChallenge, row.codeChallenge)) {
|
|
529
|
+
return oauthError("invalid_grant", "PKCE verification failed");
|
|
530
|
+
}
|
|
531
|
+
const consumed = await consumeOAuthCode(code);
|
|
532
|
+
if (!consumed)
|
|
533
|
+
return oauthError("invalid_grant", "Invalid or expired code");
|
|
534
|
+
const issuer = getMcpOAuthIssuer(event);
|
|
535
|
+
if (!issuer)
|
|
536
|
+
return oauthError("server_error", "Unable to derive issuer", 500);
|
|
537
|
+
return json(await issueTokenSet({
|
|
538
|
+
ownerEmail: row.ownerEmail,
|
|
539
|
+
orgId: row.orgId,
|
|
540
|
+
orgDomain: row.orgDomain,
|
|
541
|
+
clientId,
|
|
542
|
+
scope: row.scope,
|
|
543
|
+
resource: row.resource,
|
|
544
|
+
issuer,
|
|
545
|
+
}));
|
|
546
|
+
}
|
|
547
|
+
async function handleRefreshTokenGrant(event, body) {
|
|
548
|
+
const refreshToken = body.refresh_token;
|
|
549
|
+
const clientId = body.client_id;
|
|
550
|
+
if (!refreshToken) {
|
|
551
|
+
return oauthError("invalid_request", "refresh_token is required");
|
|
552
|
+
}
|
|
553
|
+
if (!clientId) {
|
|
554
|
+
return oauthError("invalid_request", "client_id is required");
|
|
555
|
+
}
|
|
556
|
+
const existing = await getOAuthRefreshToken(refreshToken);
|
|
557
|
+
if (!existing)
|
|
558
|
+
return oauthError("invalid_grant", "Invalid refresh token");
|
|
559
|
+
if (existing.clientId !== clientId) {
|
|
560
|
+
return oauthError("invalid_grant", "Refresh token belongs to another client");
|
|
561
|
+
}
|
|
562
|
+
const nextRefreshToken = generateOpaqueToken();
|
|
563
|
+
const row = await rotateOAuthRefreshToken({
|
|
564
|
+
oldRefreshToken: refreshToken,
|
|
565
|
+
newRefreshToken: nextRefreshToken,
|
|
566
|
+
});
|
|
567
|
+
if (!row)
|
|
568
|
+
return oauthError("invalid_grant", "Invalid refresh token");
|
|
569
|
+
const issuer = getMcpOAuthIssuer(event);
|
|
570
|
+
if (!issuer)
|
|
571
|
+
return oauthError("server_error", "Unable to derive issuer", 500);
|
|
572
|
+
const accessToken = await signMcpOAuthAccessToken({
|
|
573
|
+
ownerEmail: row.ownerEmail,
|
|
574
|
+
orgDomain: row.orgDomain,
|
|
575
|
+
clientId: row.clientId,
|
|
576
|
+
scope: row.scope,
|
|
577
|
+
resource: row.resource,
|
|
578
|
+
issuer,
|
|
579
|
+
});
|
|
580
|
+
return json({
|
|
581
|
+
access_token: accessToken,
|
|
582
|
+
token_type: "Bearer",
|
|
583
|
+
expires_in: 3600,
|
|
584
|
+
refresh_token: nextRefreshToken,
|
|
585
|
+
scope: row.scope,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
async function handleToken(event) {
|
|
589
|
+
if (getMethod(event) !== "POST") {
|
|
590
|
+
return oauthError("invalid_request", "Method not allowed", 405);
|
|
591
|
+
}
|
|
592
|
+
const body = await readOAuthParams(event);
|
|
593
|
+
switch (body.grant_type) {
|
|
594
|
+
case "authorization_code":
|
|
595
|
+
return handleAuthorizationCodeGrant(event, body);
|
|
596
|
+
case "refresh_token":
|
|
597
|
+
return handleRefreshTokenGrant(event, body);
|
|
598
|
+
default:
|
|
599
|
+
return oauthError("unsupported_grant_type", "Unsupported grant_type");
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
export async function handleMcpOAuth(event, subpath, options = {}) {
|
|
603
|
+
const path = subpath.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
604
|
+
try {
|
|
605
|
+
if (path === "authorize")
|
|
606
|
+
return await handleAuthorize(event, options);
|
|
607
|
+
if (path === "token")
|
|
608
|
+
return await handleToken(event);
|
|
609
|
+
if (path === "register")
|
|
610
|
+
return await handleRegister(event);
|
|
611
|
+
setResponseStatus(event, 404);
|
|
612
|
+
return json({ error: "Not found" }, 404);
|
|
613
|
+
}
|
|
614
|
+
catch (err) {
|
|
615
|
+
return oauthError("server_error", err?.message || "OAuth request failed", 500);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
//# sourceMappingURL=oauth-route.js.map
|