@agent-native/core 0.154.3 → 0.154.5
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/corpus/README.md +1 -1
- package/corpus/templates/content/.agents/skills/content/SKILL.md +2 -2
- package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +20 -4
- package/corpus/templates/content/actions/_blocks-field-identity.ts +464 -0
- package/corpus/templates/content/actions/_database-row-mutation.ts +1490 -0
- package/corpus/templates/content/actions/_database-utils.ts +55 -3
- package/corpus/templates/content/actions/_property-utils.ts +141 -31
- package/corpus/templates/content/actions/add-database-item.ts +62 -240
- package/corpus/templates/content/actions/configure-document-property.ts +170 -1
- package/corpus/templates/content/actions/delete-document-property.ts +56 -17
- package/corpus/templates/content/actions/delete-document.ts +3 -0
- package/corpus/templates/content/actions/edit-document.ts +18 -1
- package/corpus/templates/content/actions/export-document.ts +41 -0
- package/corpus/templates/content/actions/remove-database-items.ts +10 -0
- package/corpus/templates/content/actions/set-document-property.ts +140 -3
- package/corpus/templates/content/actions/update-database-item.ts +62 -0
- package/corpus/templates/content/actions/update-document.ts +24 -0
- package/corpus/templates/content/actions/upsert-database-item-by-key.ts +43 -796
- package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +99 -11
- package/corpus/templates/content/app/components/editor/blockFieldSaveController.ts +6 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +10 -8
- package/corpus/templates/content/app/hooks/use-content-database.ts +33 -32
- package/corpus/templates/content/app/hooks/use-document-properties.ts +4 -1
- package/corpus/templates/content/app/i18n-data.ts +20 -0
- package/corpus/templates/content/changelog/2026-08-10-database-blocks-fields-now-keep-logical-block-identity-throu.md +6 -0
- package/corpus/templates/content/changelog/2026-08-10-database-row-actions-now-validate-exact-schemas-and-safe-ret.md +6 -0
- package/corpus/templates/content/changelog/2026-08-13-blocks-field-conflicts-now-keep-the-newer-saved-version-visi.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.object.block.md +5 -4
- package/corpus/templates/content/docs/product/capabilities/content.object.blocks-field.md +9 -4
- package/corpus/templates/content/docs/product/encyclopedia.md +4 -4
- package/corpus/templates/content/parity/matrix.md +1 -1
- package/corpus/templates/content/parity/matrix.ts +2 -0
- package/corpus/templates/content/server/db/schema.ts +100 -3
- package/corpus/templates/content/server/plugins/db.ts +85 -0
- package/corpus/templates/content/shared/api.ts +78 -2
- package/corpus/templates/content/shared/blocks-field-identity.ts +554 -0
- package/corpus/templates/content/shared/document-export.ts +33 -1
- package/dist/action.d.ts +22 -0
- package/dist/action.js +27 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/extensions/url-safety.d.ts +7 -2
- package/dist/extensions/url-safety.js +27 -12
- package/dist/index.browser.d.ts +1 -1
- package/dist/index.browser.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mcp-client/oauth-client.d.ts +16 -1
- package/dist/mcp-client/oauth-client.js +177 -83
- package/dist/mcp-client/remote-store.js +46 -20
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/oauth-tokens/index.d.ts +2 -1
- package/dist/oauth-tokens/index.js +2 -1
- package/dist/oauth-tokens/lifecycle.d.ts +87 -0
- package/dist/oauth-tokens/lifecycle.js +434 -0
- package/dist/oauth-tokens/migrations.js +10 -0
- package/dist/oauth-tokens/store.d.ts +22 -0
- package/dist/oauth-tokens/store.js +141 -18
- package/dist/observability/routes.d.ts +2 -2
- package/dist/progress/routes.d.ts +1 -1
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/action-routes.js +10 -2
- package/dist/server/realtime-token.d.ts +1 -1
- package/package.json +2 -1
package/dist/collab/routes.d.ts
CHANGED
|
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
|
|
|
26
26
|
* Body: { update: string (base64), requestSource?: string }
|
|
27
27
|
*/
|
|
28
28
|
export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
29
|
-
ok?: undefined;
|
|
30
29
|
error: string;
|
|
30
|
+
ok?: undefined;
|
|
31
31
|
} | {
|
|
32
32
|
error?: undefined;
|
|
33
33
|
ok: boolean;
|
|
@@ -5,7 +5,9 @@ export declare function isBlockedExtensionUrl(url: string): boolean;
|
|
|
5
5
|
* "public hostname resolves to a private address" gap before dispatch.
|
|
6
6
|
*/
|
|
7
7
|
export declare function isBlockedExtensionUrlWithDns(url: string): Promise<boolean>;
|
|
8
|
-
export declare function createSsrfSafeDispatcher(allowedPrivateOrigins?: readonly string[]
|
|
8
|
+
export declare function createSsrfSafeDispatcher(allowedPrivateOrigins?: readonly string[], destinationUrl?: string, options?: {
|
|
9
|
+
required?: boolean;
|
|
10
|
+
}): Promise<unknown>;
|
|
9
11
|
/**
|
|
10
12
|
* SSRF-safe `fetch` for any server-side request to a user/agent-supplied URL.
|
|
11
13
|
*
|
|
@@ -14,7 +16,9 @@ export declare function createSsrfSafeDispatcher(allowedPrivateOrigins?: readonl
|
|
|
14
16
|
* 1. Pre-flight DNS-aware private-address check (isBlockedExtensionUrlWithDns)
|
|
15
17
|
* on the initial URL and on every redirect hop.
|
|
16
18
|
* 2. A connect-time dispatcher that re-checks the resolved IP at TCP-connect
|
|
17
|
-
* time (closes the DNS-rebinding TOCTOU) when
|
|
19
|
+
* time (closes the DNS-rebinding TOCTOU) when the runtime supports the
|
|
20
|
+
* Node dispatcher. Edge runtimes retain preflight DNS checks, literal and
|
|
21
|
+
* rebinding-domain rejection, and per-hop redirect validation.
|
|
18
22
|
* 3. Manual redirect handling — a public URL cannot 30x-redirect into the
|
|
19
23
|
* private network because each hop is re-validated before it is followed.
|
|
20
24
|
*
|
|
@@ -33,6 +37,7 @@ export declare function createSsrfSafeDispatcher(allowedPrivateOrigins?: readonl
|
|
|
33
37
|
*/
|
|
34
38
|
export declare function ssrfSafeFetch(url: string, init?: RequestInit, options?: {
|
|
35
39
|
maxRedirects?: number;
|
|
40
|
+
followRedirects?: boolean;
|
|
36
41
|
httpsOnly?: boolean;
|
|
37
42
|
assertUrlAllowed?: (url: string) => void | Promise<void>;
|
|
38
43
|
/**
|
|
@@ -225,25 +225,37 @@ function normalizeAllowedPrivateOriginOriginKeys(origins) {
|
|
|
225
225
|
}
|
|
226
226
|
return keys;
|
|
227
227
|
}
|
|
228
|
-
export async function createSsrfSafeDispatcher(allowedPrivateOrigins = []) {
|
|
229
|
-
// Keep the
|
|
230
|
-
//
|
|
231
|
-
// even though this dispatcher is only useful in Node server runtimes.
|
|
228
|
+
export async function createSsrfSafeDispatcher(allowedPrivateOrigins = [], destinationUrl, options = {}) {
|
|
229
|
+
// Keep the undici import opaque to Vite/Rolldown. A literal dynamic import
|
|
230
|
+
// makes browser builds try to resolve and bundle this server-only package.
|
|
232
231
|
let undici;
|
|
233
232
|
let dnsModule;
|
|
234
233
|
try {
|
|
235
|
-
const
|
|
236
|
-
undici = await
|
|
234
|
+
const undiciSpecifier = "undici";
|
|
235
|
+
undici = await import(/* @vite-ignore */ undiciSpecifier);
|
|
237
236
|
dnsModule = await import("node:dns");
|
|
238
237
|
}
|
|
239
|
-
catch {
|
|
238
|
+
catch (error) {
|
|
239
|
+
if (options.required) {
|
|
240
|
+
throw new Error("SSRF protection is unavailable because the server dispatcher could not be loaded.", { cause: error });
|
|
241
|
+
}
|
|
240
242
|
return null;
|
|
241
243
|
}
|
|
242
244
|
const { Agent } = undici;
|
|
243
245
|
const { lookup } = dnsModule;
|
|
244
246
|
const allowedPrivateOriginKeys = normalizeAllowedPrivateOriginKeys(allowedPrivateOrigins);
|
|
245
|
-
|
|
247
|
+
let destinationPort = "";
|
|
248
|
+
if (destinationUrl) {
|
|
249
|
+
const parsed = new URL(destinationUrl);
|
|
250
|
+
destinationPort =
|
|
251
|
+
parsed.port || (parsed.protocol === "https:" ? "443" : "80");
|
|
252
|
+
}
|
|
253
|
+
if (!Agent || !lookup) {
|
|
254
|
+
if (options.required) {
|
|
255
|
+
throw new Error("SSRF protection is unavailable because the server dispatcher is incomplete.");
|
|
256
|
+
}
|
|
246
257
|
return null;
|
|
258
|
+
}
|
|
247
259
|
return new Agent({
|
|
248
260
|
connect: {
|
|
249
261
|
// Override DNS lookup at connect time so the IP we hand to undici's
|
|
@@ -257,7 +269,7 @@ export async function createSsrfSafeDispatcher(allowedPrivateOrigins = []) {
|
|
|
257
269
|
? addresses
|
|
258
270
|
: [{ address: addresses, family: 4 }];
|
|
259
271
|
for (const record of list) {
|
|
260
|
-
const allowedOrigin = allowedPrivateOriginKeys.has(`${normalizeLookupHostname(hostname)}:${
|
|
272
|
+
const allowedOrigin = allowedPrivateOriginKeys.has(`${normalizeLookupHostname(hostname)}:${destinationPort}`);
|
|
261
273
|
if (isPrivateHost(record.address) && !allowedOrigin) {
|
|
262
274
|
const e = new Error(`Connect blocked: ${hostname} resolved to private address ${record.address}`);
|
|
263
275
|
e.code = "EAI_BLOCKED";
|
|
@@ -285,7 +297,9 @@ export async function createSsrfSafeDispatcher(allowedPrivateOrigins = []) {
|
|
|
285
297
|
* 1. Pre-flight DNS-aware private-address check (isBlockedExtensionUrlWithDns)
|
|
286
298
|
* on the initial URL and on every redirect hop.
|
|
287
299
|
* 2. A connect-time dispatcher that re-checks the resolved IP at TCP-connect
|
|
288
|
-
* time (closes the DNS-rebinding TOCTOU) when
|
|
300
|
+
* time (closes the DNS-rebinding TOCTOU) when the runtime supports the
|
|
301
|
+
* Node dispatcher. Edge runtimes retain preflight DNS checks, literal and
|
|
302
|
+
* rebinding-domain rejection, and per-hop redirect validation.
|
|
289
303
|
* 3. Manual redirect handling — a public URL cannot 30x-redirect into the
|
|
290
304
|
* private network because each hop is re-validated before it is followed.
|
|
291
305
|
*
|
|
@@ -304,8 +318,6 @@ export async function createSsrfSafeDispatcher(allowedPrivateOrigins = []) {
|
|
|
304
318
|
*/
|
|
305
319
|
export async function ssrfSafeFetch(url, init = {}, options = {}) {
|
|
306
320
|
const maxRedirects = options.maxRedirects ?? 3;
|
|
307
|
-
const dispatcher = (await createSsrfSafeDispatcher(options.allowedPrivateOrigins)) ??
|
|
308
|
-
undefined;
|
|
309
321
|
const allowedPrivateOrigins = normalizeAllowedPrivateOriginOriginKeys(options.allowedPrivateOrigins ?? []);
|
|
310
322
|
const isAllowedPrivateOrigin = (candidate) => {
|
|
311
323
|
if (allowedPrivateOrigins.size === 0)
|
|
@@ -333,10 +345,13 @@ export async function ssrfSafeFetch(url, init = {}, options = {}) {
|
|
|
333
345
|
...init,
|
|
334
346
|
redirect: "manual",
|
|
335
347
|
};
|
|
348
|
+
const dispatcher = await createSsrfSafeDispatcher(options.allowedPrivateOrigins, currentUrl);
|
|
336
349
|
if (dispatcher)
|
|
337
350
|
fetchOpts.dispatcher = dispatcher;
|
|
338
351
|
const response = await fetch(currentUrl, fetchOpts);
|
|
339
352
|
if (response.status >= 300 && response.status < 400) {
|
|
353
|
+
if (options.followRedirects === false)
|
|
354
|
+
return response;
|
|
340
355
|
const location = response.headers.get("location");
|
|
341
356
|
if (!location)
|
|
342
357
|
return response;
|
package/dist/index.browser.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export * from "./config.js";
|
|
|
2
2
|
export { addContextToAgentChat, appendAgentChatContextToMessage, clearAgentChatContext, formatAgentChatContextItemsForPrompt, listAgentChatContext, refreshAgentChatContext, removeAgentChatContextItem, sendToAgentChat, sendToAgentChatAndConfirm, setAgentChatContextItem, setContextToAgentChat, isEmbedMcpChatBridgeActive, useAgentChatContext, useAgentChatGenerating, useDevMode, useSendToAgentChat, AGENT_NATIVE_MCP_APP_HOST_MESSAGE_TYPES, getMcpAppHostContext, openMcpAppHostLink, requestMcpAppDisplayMode, sendMcpAppHostMessage, updateMcpAppModelContext, useMcpAppHostContext, CodeRequiredDialog, AgentNativeI18nProvider, LanguagePicker, getLocaleInitScript, localeDirection, normalizeLocaleCode, normalizeLocalePreference, normalizeLocalizationPreference, resolveLocaleFromCandidates, resolveLocaleFromPreference, useFormatters, useLocale, useT, DEFAULT_LOCALE, LOCALE_HYDRATION_GLOBAL, LOCALE_METADATA, LOCALE_STORAGE_KEY, SUPPORTED_LOCALES, useDbSync, useFileWatcher, useSession, cn, ApiKeySettings, type AgentChatContextItem, type AgentChatContextMessage, type AgentChatContextMutationOptions, type AgentChatContextRemoveOptions, type AgentChatContextSetOptions, type AgentChatContextState, type AgentChatMessage, type AgentNativeMcpAppHostMessageType, type McpAppDisplayMode, type McpAppHostChatMessage, type McpAppHostCapabilities, type McpAppHostContext, type McpAppHostContextSnapshot, type McpAppModelContextContentPart, type McpAppModelContextUpdate, type CodeRequiredDialogProps, type AuthSession, type UseAgentChatContextResult, type AgentNativeI18nCatalog, type AgentNativeI18nProviderProps, type LocaleCode, type LocaleHydrationPayload, type LocaleMessages, type LocaleMetadata, type LocalePreference, type LocalizationPreference, } from "./client/index.js";
|
|
3
3
|
export { agentChat } from "./shared/index.js";
|
|
4
4
|
export { parseArgs, camelCaseArgs } from "./scripts/parse-args.js";
|
|
5
|
-
export { defineAction, AgentActionStopError, isAgentActionStopError, type ActionHttpConfig, type AgentActionStopOptions, MCP_APP_EXTENSION_ID, MCP_APP_MIME_TYPE, MCP_APP_RESOURCE_URI_META_KEY, type ActionMcpAppConfig, type ActionMcpAppCsp, type ActionMcpAppCspBuilder, type ActionMcpAppHtmlBuilder, type ActionMcpAppPermissions, type ActionMcpAppResourceConfig, type ActionMcpAppResourceMeta, } from "./action.js";
|
|
5
|
+
export { defineAction, ActionContractError, isActionContractError, type ActionContractErrorOptions, AgentActionStopError, isAgentActionStopError, type ActionHttpConfig, type AgentActionStopOptions, MCP_APP_EXTENSION_ID, MCP_APP_MIME_TYPE, MCP_APP_RESOURCE_URI_META_KEY, type ActionMcpAppConfig, type ActionMcpAppCsp, type ActionMcpAppCspBuilder, type ActionMcpAppHtmlBuilder, type ActionMcpAppPermissions, type ActionMcpAppResourceConfig, type ActionMcpAppResourceMeta, } from "./action.js";
|
|
6
6
|
export { normalizeUserProfileName, USER_PROFILE_SETTING_KEY, type UserProfile, } from "./user-profile/index.js";
|
|
7
7
|
export { ACTION_CHAT_UI_DATA_CHART_RENDERER, ACTION_CHAT_UI_DATA_INSIGHTS_RENDERER, ACTION_CHAT_UI_DATA_TABLE_RENDERER, ACTION_CHAT_UI_DATA_WIDGET_RENDERER, ACTION_CHAT_UI_INLINE_EXTENSION_RENDERER, ACTION_CHAT_UI_WORKSPACE_FILE_RENDERER, type ActionChatUIConfig, } from "./action-ui.js";
|
|
8
8
|
export { DATA_CHART_WIDGET, DATA_INSIGHTS_WIDGET, DATA_TABLE_WIDGET, createDataChartWidgetResult, createDataInsightsWidgetResult, createDataTableWidgetResult, dataChartWidgetResultSchema, dataChartWidgetSchema, dataInsightsWidgetResultSchema, dataTableWidgetResultSchema, dataTableWidgetSchema, dataWidgetResultSchema, isDataChartWidget, isDataTableWidget, isDataWidgetResult, normalizeDataWidgetKind, normalizeDataWidgetResult, type DataChartSeriesDefinition, type DataChartWidget, type DataChartWidgetResult, type DataChartWidgetResultInput, type DataInsightsWidgetResult, type DataInsightsWidgetResultInput, type DataTableColumn, type DataTableWidget, type DataTableWidgetResult, type DataTableWidgetResultInput, type DataWidgetDisplay, type DataWidgetKind, type DataWidgetResult, type DataWidgetResultMetadata, } from "./data-widgets/index.js";
|
package/dist/index.browser.js
CHANGED
|
@@ -9,7 +9,7 @@ export { agentChat } from "./shared/index.js";
|
|
|
9
9
|
// Pure utilities (no Node.js deps — safe for browser and SSR)
|
|
10
10
|
export { parseArgs, camelCaseArgs } from "./scripts/parse-args.js";
|
|
11
11
|
// defineAction — used by template actions, no Node.js deps
|
|
12
|
-
export { defineAction, AgentActionStopError, isAgentActionStopError, MCP_APP_EXTENSION_ID, MCP_APP_MIME_TYPE, MCP_APP_RESOURCE_URI_META_KEY, } from "./action.js";
|
|
12
|
+
export { defineAction, ActionContractError, isActionContractError, AgentActionStopError, isAgentActionStopError, MCP_APP_EXTENSION_ID, MCP_APP_MIME_TYPE, MCP_APP_RESOURCE_URI_META_KEY, } from "./action.js";
|
|
13
13
|
export { normalizeUserProfileName, USER_PROFILE_SETTING_KEY, } from "./user-profile/index.js";
|
|
14
14
|
export { ACTION_CHAT_UI_DATA_CHART_RENDERER, ACTION_CHAT_UI_DATA_INSIGHTS_RENDERER, ACTION_CHAT_UI_DATA_TABLE_RENDERER, ACTION_CHAT_UI_DATA_WIDGET_RENDERER, ACTION_CHAT_UI_INLINE_EXTENSION_RENDERER, ACTION_CHAT_UI_WORKSPACE_FILE_RENDERER, } from "./action-ui.js";
|
|
15
15
|
export { DATA_CHART_WIDGET, DATA_INSIGHTS_WIDGET, DATA_TABLE_WIDGET, createDataChartWidgetResult, createDataInsightsWidgetResult, createDataTableWidgetResult, dataChartWidgetResultSchema, dataChartWidgetSchema, dataInsightsWidgetResultSchema, dataTableWidgetResultSchema, dataTableWidgetSchema, dataWidgetResultSchema, isDataChartWidget, isDataTableWidget, isDataWidgetResult, normalizeDataWidgetKind, normalizeDataWidgetResult, } from "./data-widgets/index.js";
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export * from "./config.js";
|
|
2
2
|
export { createProductionAgentHandler, type ActionEntry, type ScriptEntry, type ProductionAgentOptions, type ActionTool, type ScriptTool, type AgentMessage, type AgentChatRequest, type AgentChatEvent, type AgentChatAttachment, DEFAULT_MODEL, TripWire, type Processor, type ProcessorState, type ProcessorAbort, } from "./agent/index.js";
|
|
3
|
-
export { defineAction, AgentActionStopError, isAgentActionStopError, type ActionDefinition, type ActionHttpConfig, type AgentActionStopOptions, type PublicAgentActionConfig, type ActionDeepLink, type ActionLinkBuilder, MCP_APP_EXTENSION_ID, MCP_APP_MIME_TYPE, MCP_APP_RESOURCE_URI_META_KEY, type ActionMcpAppConfig, type ActionMcpAppCsp, type ActionMcpAppCspBuilder, type ActionMcpAppHtmlBuilder, type ActionMcpAppPermissions, type ActionMcpAppResourceConfig, type ActionMcpAppResourceMeta, } from "./action.js";
|
|
3
|
+
export { defineAction, ActionContractError, isActionContractError, type ActionContractErrorOptions, AgentActionStopError, isAgentActionStopError, type ActionDefinition, type ActionHttpConfig, type AgentActionStopOptions, type PublicAgentActionConfig, type ActionDeepLink, type ActionLinkBuilder, MCP_APP_EXTENSION_ID, MCP_APP_MIME_TYPE, MCP_APP_RESOURCE_URI_META_KEY, type ActionMcpAppConfig, type ActionMcpAppCsp, type ActionMcpAppCspBuilder, type ActionMcpAppHtmlBuilder, type ActionMcpAppPermissions, type ActionMcpAppResourceConfig, type ActionMcpAppResourceMeta, } from "./action.js";
|
|
4
4
|
export * from "./user-profile/index.js";
|
|
5
5
|
export { embedApp, MCP_APP_REQUEST_ORIGIN_CSP_SOURCE, type EmbedAppOptions, } from "./mcp/embed-app.js";
|
|
6
6
|
export { embedRoute, type EmbedRouteContext, type EmbedRouteOptions, type EmbedRoutePathBuilder, type EmbedRouteResult, } from "./mcp/embed-route.js";
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ export * from "./config.js";
|
|
|
5
5
|
export { createProductionAgentHandler, DEFAULT_MODEL,
|
|
6
6
|
// In-loop processors (observers/guardrails for runAgentLoop)
|
|
7
7
|
TripWire, } from "./agent/index.js";
|
|
8
|
-
export { defineAction, AgentActionStopError, isAgentActionStopError, MCP_APP_EXTENSION_ID, MCP_APP_MIME_TYPE, MCP_APP_RESOURCE_URI_META_KEY, } from "./action.js";
|
|
8
|
+
export { defineAction, ActionContractError, isActionContractError, AgentActionStopError, isAgentActionStopError, MCP_APP_EXTENSION_ID, MCP_APP_MIME_TYPE, MCP_APP_RESOURCE_URI_META_KEY, } from "./action.js";
|
|
9
9
|
export * from "./user-profile/index.js";
|
|
10
10
|
export { embedApp, MCP_APP_REQUEST_ORIGIN_CSP_SOURCE, } from "./mcp/embed-app.js";
|
|
11
11
|
export { embedRoute, } from "./mcp/embed-route.js";
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* and refresh boundary.
|
|
8
8
|
*/
|
|
9
9
|
import { type AuthorizationServerMetadata, type OAuthClientInformationContext, type OAuthClientMetadata, type OAuthClientProvider, type OAuthDiscoveryState, OAuthProtectedResourceMetadata, type StoredOAuthClientInformation, type StoredOAuthTokens, OAuthTokens } from "@modelcontextprotocol/client";
|
|
10
|
+
import { type OAuthCredential, type OAuthCredentialState, type OAuthRevocationResult } from "../oauth-tokens/lifecycle.js";
|
|
10
11
|
export type McpOAuthDiscoveryState = OAuthDiscoveryState;
|
|
11
|
-
export interface McpOAuthCredentialBundle {
|
|
12
|
+
export interface McpOAuthCredentialBundle extends OAuthCredential {
|
|
12
13
|
serverUrl: string;
|
|
13
14
|
clientInformation: StoredOAuthClientInformation;
|
|
14
15
|
discoveryState?: McpOAuthDiscoveryState;
|
|
@@ -89,12 +90,26 @@ export declare function readMcpOAuthCredentials(options: {
|
|
|
89
90
|
key: string;
|
|
90
91
|
scope: "user" | "org";
|
|
91
92
|
scopeId: string;
|
|
93
|
+
serverUrl?: string;
|
|
92
94
|
}): Promise<McpOAuthCredentialBundle | null>;
|
|
95
|
+
export declare function getMcpOAuthConnectionState(options: {
|
|
96
|
+
key: string;
|
|
97
|
+
scope: "user" | "org";
|
|
98
|
+
scopeId: string;
|
|
99
|
+
serverUrl: string;
|
|
100
|
+
}): Promise<OAuthCredentialState<McpOAuthCredentialBundle>>;
|
|
93
101
|
export declare function deleteMcpOAuthCredentials(options: {
|
|
94
102
|
key: string;
|
|
95
103
|
scope: "user" | "org";
|
|
96
104
|
scopeId: string;
|
|
105
|
+
serverUrl?: string;
|
|
97
106
|
}): Promise<boolean>;
|
|
107
|
+
export declare function revokeMcpOAuthCredentials(options: {
|
|
108
|
+
key: string;
|
|
109
|
+
scope: "user" | "org";
|
|
110
|
+
scopeId: string;
|
|
111
|
+
serverUrl: string;
|
|
112
|
+
}): Promise<OAuthRevocationResult>;
|
|
98
113
|
/**
|
|
99
114
|
* Resolve an access token for the MCP manager. Refreshing happens only when a
|
|
100
115
|
* token is near expiry, so ordinary manager reconfiguration does not perform
|
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
* and refresh boundary.
|
|
8
8
|
*/
|
|
9
9
|
import { auth, refreshAuthorization, validateAuthorizationResponseIssuer, } from "@modelcontextprotocol/client";
|
|
10
|
-
import {
|
|
10
|
+
import { ssrfSafeFetch } from "../extensions/url-safety.js";
|
|
11
|
+
import { readOAuthCredentialState, resolveOAuthCredentialAccess, revokeOAuthCredential, saveOAuthCredential, } from "../oauth-tokens/lifecycle.js";
|
|
11
12
|
import { validateRemoteUrl } from "./remote-url.js";
|
|
12
13
|
const TOKEN_EXPIRY_SKEW_MS = 60_000;
|
|
13
14
|
const MAX_OAUTH_REDIRECTS = 5;
|
|
15
|
+
const MCP_OAUTH_PRIVATE_ORIGINS_ENV = "AGENT_NATIVE_MCP_OAUTH_PRIVATE_ORIGINS";
|
|
14
16
|
function checkedRemoteUrl(value, label) {
|
|
15
17
|
const result = validateRemoteUrl(String(value));
|
|
16
18
|
if (!result.ok || !result.url) {
|
|
@@ -18,12 +20,35 @@ function checkedRemoteUrl(value, label) {
|
|
|
18
20
|
}
|
|
19
21
|
return result.url;
|
|
20
22
|
}
|
|
23
|
+
function canonicalServerUrl(value) {
|
|
24
|
+
return checkedRemoteUrl(value, "server").toString();
|
|
25
|
+
}
|
|
26
|
+
function serverUrlsMatch(stored, canonical) {
|
|
27
|
+
try {
|
|
28
|
+
return canonicalServerUrl(stored) === canonical;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// coercion-ok: an invalid stored URL is an explicit identity mismatch.
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function configuredPrivateOrigins() {
|
|
36
|
+
return (process.env[MCP_OAUTH_PRIVATE_ORIGINS_ENV] ?? "")
|
|
37
|
+
.split(",")
|
|
38
|
+
.map((origin) => origin.trim())
|
|
39
|
+
.filter(Boolean);
|
|
40
|
+
}
|
|
21
41
|
function guardedOAuthFetch() {
|
|
42
|
+
const allowedPrivateOrigins = configuredPrivateOrigins();
|
|
22
43
|
return async (url, init) => {
|
|
23
44
|
let currentUrl = checkedRemoteUrl(url, "request");
|
|
24
45
|
let currentInit = { ...init, redirect: "manual" };
|
|
25
46
|
for (let redirectCount = 0;; redirectCount += 1) {
|
|
26
|
-
const response = await
|
|
47
|
+
const response = await ssrfSafeFetch(currentUrl.href, currentInit, {
|
|
48
|
+
maxRedirects: 0,
|
|
49
|
+
followRedirects: false,
|
|
50
|
+
allowedPrivateOrigins,
|
|
51
|
+
});
|
|
27
52
|
if (![301, 302, 303, 307, 308].includes(response.status)) {
|
|
28
53
|
return response;
|
|
29
54
|
}
|
|
@@ -42,10 +67,15 @@ function guardedOAuthFetch() {
|
|
|
42
67
|
}
|
|
43
68
|
await response.body?.cancel().catch(() => undefined);
|
|
44
69
|
const nextHeaders = new Headers(currentInit.headers);
|
|
45
|
-
|
|
70
|
+
const changesOrigin = nextUrl.origin !== currentUrl.origin;
|
|
71
|
+
if (changesOrigin) {
|
|
46
72
|
nextHeaders.delete("authorization");
|
|
47
73
|
nextHeaders.delete("cookie");
|
|
48
74
|
nextHeaders.delete("proxy-authorization");
|
|
75
|
+
if ((response.status === 307 || response.status === 308) &&
|
|
76
|
+
currentInit.body != null) {
|
|
77
|
+
throw new Error("MCP OAuth redirect cannot forward a request body across origins.");
|
|
78
|
+
}
|
|
49
79
|
}
|
|
50
80
|
const nextInit = {
|
|
51
81
|
...currentInit,
|
|
@@ -64,6 +94,19 @@ function guardedOAuthFetch() {
|
|
|
64
94
|
}
|
|
65
95
|
};
|
|
66
96
|
}
|
|
97
|
+
async function guardedRevocationFetch(url, init) {
|
|
98
|
+
const endpoint = checkedRemoteUrl(url, "revocation endpoint");
|
|
99
|
+
return ssrfSafeFetch(endpoint.href, init, {
|
|
100
|
+
maxRedirects: 0,
|
|
101
|
+
allowedPrivateOrigins: configuredPrivateOrigins(),
|
|
102
|
+
assertUrlAllowed: (candidate) => {
|
|
103
|
+
const checked = checkedRemoteUrl(candidate, "revocation redirect");
|
|
104
|
+
if (checked.origin !== endpoint.origin) {
|
|
105
|
+
throw new Error("MCP OAuth revocation redirect changed origin.");
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
}
|
|
67
110
|
function validateDiscoveryUrls(state) {
|
|
68
111
|
checkedRemoteUrl(state.authorizationServerUrl, "authorization server");
|
|
69
112
|
if (state.resourceMetadataUrl) {
|
|
@@ -85,13 +128,18 @@ function validateDiscoveryUrls(state) {
|
|
|
85
128
|
}
|
|
86
129
|
}
|
|
87
130
|
}
|
|
88
|
-
function credentialOwner(options) {
|
|
89
|
-
return `${options.scope}:${options.scopeId}`;
|
|
90
|
-
}
|
|
91
131
|
function issuerForDiscovery(discovery) {
|
|
92
132
|
const issuer = discovery?.authorizationServerMetadata?.issuer;
|
|
93
133
|
return typeof issuer === "string" && issuer ? issuer : undefined;
|
|
94
134
|
}
|
|
135
|
+
function credentialIdentity(options) {
|
|
136
|
+
return {
|
|
137
|
+
provider: "mcp",
|
|
138
|
+
accountId: options.key,
|
|
139
|
+
resource: options.serverUrl,
|
|
140
|
+
owner: { scope: options.scope, id: options.scopeId },
|
|
141
|
+
};
|
|
142
|
+
}
|
|
95
143
|
function withIssuer(value, context) {
|
|
96
144
|
if (value.issuer && context?.issuer && value.issuer !== context.issuer) {
|
|
97
145
|
throw new Error("MCP OAuth credential issuer does not match discovery");
|
|
@@ -270,24 +318,28 @@ export function tokenExpiresAt(tokens) {
|
|
|
270
318
|
return Date.now() + expiresIn * 1_000;
|
|
271
319
|
}
|
|
272
320
|
export async function saveMcpOAuthCredentials(options) {
|
|
273
|
-
checkedRemoteUrl(options.credentials.serverUrl, "server");
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
321
|
+
const serverUrl = checkedRemoteUrl(options.credentials.serverUrl, "server").toString();
|
|
322
|
+
const credentials = { ...options.credentials, serverUrl };
|
|
323
|
+
if (credentials.discoveryState) {
|
|
324
|
+
validateDiscoveryUrls(credentials.discoveryState);
|
|
325
|
+
}
|
|
326
|
+
await saveOAuthCredential(credentialIdentity({
|
|
327
|
+
...options,
|
|
328
|
+
serverUrl,
|
|
329
|
+
}), credentials, { legacyAccountKey: true });
|
|
278
330
|
}
|
|
279
331
|
export async function readMcpOAuthCredentials(options) {
|
|
280
|
-
|
|
281
|
-
if (!stored)
|
|
332
|
+
if (!options.serverUrl)
|
|
282
333
|
return null;
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
334
|
+
const serverUrl = canonicalServerUrl(options.serverUrl);
|
|
335
|
+
const state = await getMcpOAuthConnectionState({
|
|
336
|
+
...options,
|
|
337
|
+
serverUrl,
|
|
338
|
+
});
|
|
339
|
+
if (state.kind === "missing" || state.kind === "malformed")
|
|
288
340
|
return null;
|
|
289
|
-
|
|
290
|
-
if (!
|
|
341
|
+
const parsed = state.credential;
|
|
342
|
+
if (!serverUrlsMatch(parsed.serverUrl, serverUrl))
|
|
291
343
|
return null;
|
|
292
344
|
if (parsed.discoveryState) {
|
|
293
345
|
try {
|
|
@@ -297,10 +349,63 @@ export async function readMcpOAuthCredentials(options) {
|
|
|
297
349
|
return null;
|
|
298
350
|
}
|
|
299
351
|
}
|
|
300
|
-
return parsed;
|
|
352
|
+
return { ...parsed, serverUrl };
|
|
353
|
+
}
|
|
354
|
+
export async function getMcpOAuthConnectionState(options) {
|
|
355
|
+
const serverUrl = canonicalServerUrl(options.serverUrl);
|
|
356
|
+
return readOAuthCredentialState(credentialIdentity({ ...options, serverUrl }), {
|
|
357
|
+
allowLegacy: true,
|
|
358
|
+
legacyAccountKey: true,
|
|
359
|
+
validateCredential: (credential) => serverUrlsMatch(credential.serverUrl, serverUrl),
|
|
360
|
+
});
|
|
301
361
|
}
|
|
302
362
|
export async function deleteMcpOAuthCredentials(options) {
|
|
303
|
-
|
|
363
|
+
if (!options.serverUrl)
|
|
364
|
+
return false;
|
|
365
|
+
const serverUrl = canonicalServerUrl(options.serverUrl);
|
|
366
|
+
const identity = credentialIdentity({
|
|
367
|
+
...options,
|
|
368
|
+
serverUrl,
|
|
369
|
+
});
|
|
370
|
+
const result = await revokeOAuthCredential(identity, {
|
|
371
|
+
allowLegacy: true,
|
|
372
|
+
legacyAccountKey: true,
|
|
373
|
+
validateCredential: (credential) => serverUrlsMatch(credential.serverUrl, serverUrl),
|
|
374
|
+
});
|
|
375
|
+
return result.local === "deleted";
|
|
376
|
+
}
|
|
377
|
+
export async function revokeMcpOAuthCredentials(options) {
|
|
378
|
+
const serverUrl = canonicalServerUrl(options.serverUrl);
|
|
379
|
+
const identity = credentialIdentity({ ...options, serverUrl });
|
|
380
|
+
return revokeOAuthCredential(identity, {
|
|
381
|
+
allowLegacy: true,
|
|
382
|
+
legacyAccountKey: true,
|
|
383
|
+
validateCredential: (credential) => serverUrlsMatch(credential.serverUrl, serverUrl),
|
|
384
|
+
revoke: async ({ credential }) => {
|
|
385
|
+
const endpoint = credential.discoveryState?.authorizationServerMetadata?.revocation_endpoint;
|
|
386
|
+
if (!endpoint)
|
|
387
|
+
return "unsupported";
|
|
388
|
+
const token = credential.tokens.refresh_token ?? credential.tokens.access_token;
|
|
389
|
+
const body = new URLSearchParams({
|
|
390
|
+
token,
|
|
391
|
+
token_type_hint: credential.tokens.refresh_token
|
|
392
|
+
? "refresh_token"
|
|
393
|
+
: "access_token",
|
|
394
|
+
client_id: credential.clientInformation.client_id,
|
|
395
|
+
});
|
|
396
|
+
const response = await guardedRevocationFetch(endpoint, {
|
|
397
|
+
method: "POST",
|
|
398
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
399
|
+
body,
|
|
400
|
+
});
|
|
401
|
+
if (!response.ok) {
|
|
402
|
+
await response.body?.cancel().catch(() => undefined);
|
|
403
|
+
throw new Error("MCP OAuth revocation failed.");
|
|
404
|
+
}
|
|
405
|
+
await response.body?.cancel().catch(() => undefined);
|
|
406
|
+
return "succeeded";
|
|
407
|
+
},
|
|
408
|
+
});
|
|
304
409
|
}
|
|
305
410
|
/**
|
|
306
411
|
* Resolve an access token for the MCP manager. Refreshing happens only when a
|
|
@@ -308,66 +413,55 @@ export async function deleteMcpOAuthCredentials(options) {
|
|
|
308
413
|
* a network request for every connector.
|
|
309
414
|
*/
|
|
310
415
|
export async function getMcpOAuthAccessToken(options) {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const credentials = await readMcpOAuthCredentials(options);
|
|
314
|
-
if (!credentials || credentials.serverUrl !== options.serverUrl)
|
|
416
|
+
const validation = validateRemoteUrl(options.serverUrl);
|
|
417
|
+
if (!validation.ok || !validation.url)
|
|
315
418
|
return null;
|
|
316
|
-
const
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
});
|
|
365
|
-
return nextTokens.access_token;
|
|
366
|
-
}
|
|
367
|
-
catch {
|
|
368
|
-
// A still-valid token can survive a transient refresh failure. Once it has
|
|
369
|
-
// expired, omit it so callers surface reauthorization instead of retrying
|
|
370
|
-
// a credential that can never authenticate.
|
|
371
|
-
return tokenIsExpired ? null : accessToken;
|
|
372
|
-
}
|
|
419
|
+
const serverUrl = validation.url.toString();
|
|
420
|
+
const result = await resolveOAuthCredentialAccess(credentialIdentity({ ...options, serverUrl }), {
|
|
421
|
+
allowLegacy: true,
|
|
422
|
+
legacyAccountKey: true,
|
|
423
|
+
validateCredential: (credential) => serverUrlsMatch(credential.serverUrl, serverUrl),
|
|
424
|
+
expirySkewMs: TOKEN_EXPIRY_SKEW_MS,
|
|
425
|
+
refresh: async ({ credential: credentials }) => {
|
|
426
|
+
const refreshToken = credentials.tokens.refresh_token;
|
|
427
|
+
const discovery = credentials.discoveryState;
|
|
428
|
+
if (!refreshToken || !discovery?.authorizationServerUrl) {
|
|
429
|
+
throw new Error("MCP OAuth refresh is unavailable.");
|
|
430
|
+
}
|
|
431
|
+
const expectedIssuer = issuerForDiscovery(discovery);
|
|
432
|
+
if (!expectedIssuer ||
|
|
433
|
+
credentials.clientInformation.issuer !== expectedIssuer ||
|
|
434
|
+
credentials.tokens.issuer !== expectedIssuer) {
|
|
435
|
+
throw new Error("MCP OAuth refresh issuer binding is invalid.");
|
|
436
|
+
}
|
|
437
|
+
const resource = discovery.resourceMetadata?.resource
|
|
438
|
+
? checkedRemoteUrl(discovery.resourceMetadata.resource, "resource")
|
|
439
|
+
: undefined;
|
|
440
|
+
const authorizationServerUrl = checkedRemoteUrl(discovery.authorizationServerUrl, "authorization server");
|
|
441
|
+
const refreshed = await refreshAuthorization(authorizationServerUrl, {
|
|
442
|
+
metadata: discovery.authorizationServerMetadata,
|
|
443
|
+
clientInformation: credentials.clientInformation,
|
|
444
|
+
refreshToken,
|
|
445
|
+
resource,
|
|
446
|
+
fetchFn: guardedOAuthFetch(),
|
|
447
|
+
});
|
|
448
|
+
const nextTokens = {
|
|
449
|
+
...credentials.tokens,
|
|
450
|
+
...refreshed,
|
|
451
|
+
issuer: expectedIssuer,
|
|
452
|
+
...(refreshed.refresh_token
|
|
453
|
+
? { refresh_token: refreshed.refresh_token }
|
|
454
|
+
: credentials.tokens.refresh_token
|
|
455
|
+
? { refresh_token: credentials.tokens.refresh_token }
|
|
456
|
+
: {}),
|
|
457
|
+
};
|
|
458
|
+
const next = {
|
|
459
|
+
...credentials,
|
|
460
|
+
tokens: nextTokens,
|
|
461
|
+
tokenExpiresAt: tokenExpiresAt(nextTokens),
|
|
462
|
+
};
|
|
463
|
+
return next;
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
return result.accessToken;
|
|
373
467
|
}
|