@agent-native/core 0.168.3 → 0.168.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.
@@ -161,6 +161,8 @@ export declare function shouldAllowAgentChatSurfaceSettingsMode(mode: AgentChatS
161
161
  export declare function AgentChatSurface({ mode, className, defaultMode, showHeader, showTabBar, isFullscreen, style, chatViewTransition, showPageNewChatButton, ...props }: AgentChatSurfaceProps): React.JSX.Element;
162
162
  export interface AgentSidebarProps {
163
163
  children: React.ReactNode;
164
+ /** Keep the app surface mounted while temporarily disabling the chat panel. */
165
+ enabled?: boolean;
164
166
  /** Placeholder text for the empty chat state */
165
167
  emptyStateText?: string;
166
168
  /** Suggestion prompts shown when no messages */
@@ -260,7 +262,7 @@ export interface AgentSidebarProps {
260
262
  * Wraps app content with a toggleable agent sidebar.
261
263
  * Use AgentToggleButton in your header to open/close it.
262
264
  */
263
- export declare function AgentSidebar({ children, emptyStateText, suggestions, dynamicSuggestions, composerToolbarSlot, composerSlot, onComposerTextChange, imageModelMenu, availableAgents, availableModels, modelListLoading, selectedAgent, onAgentChange, onConnectLocalRuntime, onConnectProvider, runtime, adapterReloadKey, threadFooterSlot, defaultSidebarWidth, sidebarWidth, position, defaultOpen, animateMobile, animateDesktop, chatViewTransition, chatViewTransitionHandoff, storageKey, openStorageKey, apiUrl, agentChatSurface, desktopIdentityUnauthenticated, desktopIdentityAuthenticated, showTabBar, suppressInlineOpenApp, composerPlaceholder, openOnChatRunning, onFullscreenRequest, scope, isolateHistoryByScope, showScopeBadge, browserTabId, threadUrlSync, agentPageHref, suppressFirstRunOnboarding, }: AgentSidebarProps): React.JSX.Element;
265
+ export declare function AgentSidebar({ children, enabled, emptyStateText, suggestions, dynamicSuggestions, composerToolbarSlot, composerSlot, onComposerTextChange, imageModelMenu, availableAgents, availableModels, modelListLoading, selectedAgent, onAgentChange, onConnectLocalRuntime, onConnectProvider, runtime, adapterReloadKey, threadFooterSlot, defaultSidebarWidth, sidebarWidth, position, defaultOpen, animateMobile, animateDesktop, chatViewTransition, chatViewTransitionHandoff, storageKey, openStorageKey, apiUrl, agentChatSurface, desktopIdentityUnauthenticated, desktopIdentityAuthenticated, showTabBar, suppressInlineOpenApp, composerPlaceholder, openOnChatRunning, onFullscreenRequest, scope, isolateHistoryByScope, showScopeBadge, browserTabId, threadUrlSync, agentPageHref, suppressFirstRunOnboarding, }: AgentSidebarProps): React.JSX.Element;
264
266
  /**
265
267
  * Focus the agent chat composer input.
266
268
  * Opens the sidebar if closed, then focuses the text input.
@@ -1418,7 +1418,7 @@ export function AgentChatSurface({ mode = "panel", className, defaultMode = "cha
1418
1418
  * Wraps app content with a toggleable agent sidebar.
1419
1419
  * Use AgentToggleButton in your header to open/close it.
1420
1420
  */
1421
- export function AgentSidebar({ children, emptyStateText = "How can I help you?", suggestions, dynamicSuggestions, composerToolbarSlot, composerSlot, onComposerTextChange, imageModelMenu, availableAgents, availableModels, modelListLoading, selectedAgent, onAgentChange, onConnectLocalRuntime, onConnectProvider, runtime, adapterReloadKey, threadFooterSlot, defaultSidebarWidth, sidebarWidth, position = "right", defaultOpen = false, animateMobile = true, animateDesktop = true, chatViewTransition = false, chatViewTransitionHandoff = false, storageKey, openStorageKey, apiUrl, agentChatSurface, desktopIdentityUnauthenticated, desktopIdentityAuthenticated, showTabBar = true, suppressInlineOpenApp, composerPlaceholder, openOnChatRunning = false, onFullscreenRequest, scope, isolateHistoryByScope = false, showScopeBadge, browserTabId, threadUrlSync, agentPageHref, suppressFirstRunOnboarding = false, }) {
1421
+ export function AgentSidebar({ children, enabled = true, emptyStateText = "How can I help you?", suggestions, dynamicSuggestions, composerToolbarSlot, composerSlot, onComposerTextChange, imageModelMenu, availableAgents, availableModels, modelListLoading, selectedAgent, onAgentChange, onConnectLocalRuntime, onConnectProvider, runtime, adapterReloadKey, threadFooterSlot, defaultSidebarWidth, sidebarWidth, position = "right", defaultOpen = false, animateMobile = true, animateDesktop = true, chatViewTransition = false, chatViewTransitionHandoff = false, storageKey, openStorageKey, apiUrl, agentChatSurface, desktopIdentityUnauthenticated, desktopIdentityAuthenticated, showTabBar = true, suppressInlineOpenApp, composerPlaceholder, openOnChatRunning = false, onFullscreenRequest, scope, isolateHistoryByScope = false, showScopeBadge, browserTabId, threadUrlSync, agentPageHref, suppressFirstRunOnboarding = false, }) {
1422
1422
  const staticHostedHarnessEnabled = isHostedHarnessConfigured(injectedAgentNativeConfig().harness);
1423
1423
  const hostedHarnessQuery = useActionQuery("get-hosted-harness-config", undefined, { enabled: staticHostedHarnessEnabled });
1424
1424
  const hostedHarnessStatus = hostedHarnessQuery.data;
@@ -1596,7 +1596,8 @@ export function AgentSidebar({ children, emptyStateText = "How can I help you?",
1596
1596
  const [hasFrameSidebarState, setHasFrameSidebarState] = useState(false);
1597
1597
  const [backgroundPanelActive, setBackgroundPanelActive] = useState(false);
1598
1598
  const [runningTabIds, setRunningTabIds] = useState(() => new Set());
1599
- const shouldMountPanel = !isPerAppChatHosted &&
1599
+ const shouldMountPanel = enabled &&
1600
+ !isPerAppChatHosted &&
1600
1601
  !presentationMode &&
1601
1602
  (!frameCodeMode || !shouldParentFrameOwnAgentPanel()) &&
1602
1603
  (open || backgroundPanelActive || runningTabIds.size > 0);
@@ -1612,9 +1613,10 @@ export function AgentSidebar({ children, emptyStateText = "How can I help you?",
1612
1613
  if (frameOwned && !hasFrameSidebarState && !isPerAppChatHosted)
1613
1614
  return;
1614
1615
  dispatchAgentSidebarStateChange({
1615
- open: isPerAppChatHosted
1616
- ? perAppChatState.open
1617
- : !presentationMode && (frameOwned ? frameSidebarOpen : open),
1616
+ open: enabled &&
1617
+ (isPerAppChatHosted
1618
+ ? perAppChatState.open
1619
+ : !presentationMode && (frameOwned ? frameSidebarOpen : open)),
1618
1620
  source: frameOwned ? "frame" : "app",
1619
1621
  mode: frameOwned ? "code" : "app",
1620
1622
  });
@@ -1626,6 +1628,7 @@ export function AgentSidebar({ children, emptyStateText = "How can I help you?",
1626
1628
  hasFrameSidebarState,
1627
1629
  isPerAppChatHosted,
1628
1630
  perAppChatState.open,
1631
+ enabled,
1629
1632
  ]);
1630
1633
  useEffect(() => {
1631
1634
  if (!isPerAppChatSidebar)
@@ -1633,7 +1636,7 @@ export function AgentSidebar({ children, emptyStateText = "How can I help you?",
1633
1636
  const frameOwned = frameCodeMode && shouldParentFrameOwnAgentPanel();
1634
1637
  if (frameOwned && !hasFrameSidebarState)
1635
1638
  return;
1636
- const openState = !presentationMode && (frameOwned ? frameSidebarOpen : open);
1639
+ const openState = enabled && !presentationMode && (frameOwned ? frameSidebarOpen : open);
1637
1640
  const message = buildAppChatSidebarStateMessage(openState);
1638
1641
  window.dispatchEvent(new CustomEvent(APP_CHAT_SIDEBAR_STATE_EVENT, {
1639
1642
  detail: message.data,
@@ -1656,6 +1659,7 @@ export function AgentSidebar({ children, emptyStateText = "How can I help you?",
1656
1659
  isPerAppChatSidebar,
1657
1660
  open,
1658
1661
  presentationMode,
1662
+ enabled,
1659
1663
  ]);
1660
1664
  useEffect(() => {
1661
1665
  const preparePanel = () => setBackgroundPanelActive(true);
@@ -1968,10 +1972,9 @@ export function AgentSidebar({ children, emptyStateText = "How can I help you?",
1968
1972
  }
1969
1973
  };
1970
1974
  }, [shouldMountPanel, sidebarAnimationEnabled]);
1971
- const shouldRenderPanel = sidebarAnimationEnabled
1972
- ? renderAnimatedPanel
1973
- : shouldMountPanel;
1974
- const panelOpen = open && shouldMountPanel;
1975
+ const shouldRenderPanel = enabled &&
1976
+ (sidebarAnimationEnabled ? renderAnimatedPanel : shouldMountPanel);
1977
+ const panelOpen = enabled && open && shouldMountPanel;
1975
1978
  const panelLayout = isMobile
1976
1979
  ? "mobile"
1977
1980
  : wideDrawerEnabled
@@ -2064,6 +2067,7 @@ export function AgentSidebar({ children, emptyStateText = "How can I help you?",
2064
2067
  return (_jsx(AgentSidebarOnboardingContext.Provider, { value: true, children: _jsxs(RealtimeVoiceModeProvider, { browserTabId: browserTabId, children: [showFirstRunOnboarding && (_jsx(Suspense, { fallback: null, children: _jsx(FirstRunOnboarding, {}) })), _jsxs("div", { className: "agent-sidebar-shell flex min-w-0 flex-1 h-screen overflow-hidden", "data-agent-sidebar-position": effectivePosition, "data-agent-native-hosted-harness-ui": hostedHarnessUi ? "desktop" : undefined, "data-agent-native-hosted-chat": isPerAppChatHosted ? "true" : undefined, "data-agent-sidebar-resizing": isResizing ? "true" : undefined, children: [isMobile &&
2065
2068
  !isPerAppChatHosted &&
2066
2069
  !presentationMode &&
2070
+ enabled &&
2067
2071
  (mobileAnimationEnabled ? shouldRenderPanel : open) && (_jsx("div", { className: cn("agent-sidebar-backdrop fixed inset-0 bg-foreground/40", mobileAnimationEnabled && !panelOpen && "pointer-events-none"), "data-agent-sidebar-animation": mobileAnimationEnabled ? "mobile" : undefined, "data-agent-sidebar-state": panelOpen ? "open" : "closed", style: { zIndex: SIDEBAR_OVERLAY_Z_INDEX - 1 }, onClick: () => setOpenPersisted(false) })), shouldMountPanel ? _jsx(URLSync, { browserTabId: browserTabId }) : null, isResizing ? (_jsx("div", { "aria-hidden": "true", className: "agent-sidebar-resize-overlay" })) : null, isLeft && !presentationMode ? sidebar : null, isLeft && !presentationMode ? drawerPlaceholder : null, _jsx("div", { className: "agent-sidebar-main-surface flex flex-1 flex-col overflow-auto min-w-0", "data-agent-sidebar-main-position": position, "data-agent-sidebar-main-state": !isMobile && !presentationMode && panelOpen ? "open" : "closed", "data-agent-sidebar-resizing": isResizing ? "true" : undefined, children: _jsx(ScreenRefreshBoundary, { children: children }) }), !isLeft && !presentationMode ? drawerPlaceholder : null, !isLeft && !presentationMode ? sidebar : null] })] }) }));
2068
2072
  }
2069
2073
  /**
@@ -345,6 +345,8 @@ export interface AssistantChatProps {
345
345
  composerDisabledPlaceholder?: string;
346
346
  /** When true, skip the restore skeleton (used for freshly created threads with no messages) */
347
347
  isNewThread?: boolean;
348
+ /** Replace an active tab when its saved thread no longer exists. */
349
+ onThreadRestoreNotFound?: () => void;
348
350
  /** Defer restore until the owning thread list has reconciled the active id. */
349
351
  isThreadStateLoading?: boolean;
350
352
  /** Called when a slash command (e.g. /clear, /help) is executed */
@@ -1387,7 +1387,7 @@ function approvalResolutionIdentity(approvalKey, toolCallId,
1387
1387
  askId) {
1388
1388
  return `${toolCallId ?? ""}\u0000${approvalKey}\u0000${askId ?? ""}`;
1389
1389
  }
1390
- const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateText, suggestions, dynamicSuggestions, threadFooterSlot, emptyStateAddon, showHeader = true, onSwitchToCli, className, apiUrl, tabId, browserTabId, threadId, contextScope, isolateHistoryByScope = false, contextNamespace, isActiveComposer = true, onMessageCountChange, onSaveThread, onGenerateTitle, composerSlot, onComposerTextChange, composerAreaClassName, composerPlaceholder, missingApiKeySetupLayout = "default", composerLayoutVariant = "default", centerComposerWhenEmpty = false, emptyStateDisplay = "default", composerToolbarSlot, composerExtraActionButton, showModelSelector = true, composerDisabled = false, composerDisabledPlaceholder, isNewThread, isThreadStateLoading, onSlashCommand, execMode, onExecModeChange, approvalActions, planModeDisabled, planModeDisabledReason, selectedModel, defaultModel, selectedEngine, selectedEffort, availableModels, modelListLoading, onModelChange, onEffortChange, availableAgents, selectedAgent, hostedHarness, onAgentChange, imageModelMenu, onForkChat, onConnectProvider, onConnectLocalRuntime, plusMenuMode = "full", providerStatusChecksEnabled = true, loadHistoryRepository, historyReloadKey, externalStreaming = false, agentChatSurface = "app", desktopIdentityUnauthenticated = false, desktopIdentityAuthenticated = false, suppressInlineOpenApp = false, }, ref) {
1390
+ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateText, suggestions, dynamicSuggestions, threadFooterSlot, emptyStateAddon, showHeader = true, onSwitchToCli, className, apiUrl, tabId, browserTabId, threadId, contextScope, isolateHistoryByScope = false, contextNamespace, isActiveComposer = true, onMessageCountChange, onSaveThread, onGenerateTitle, composerSlot, onComposerTextChange, composerAreaClassName, composerPlaceholder, missingApiKeySetupLayout = "default", composerLayoutVariant = "default", centerComposerWhenEmpty = false, emptyStateDisplay = "default", composerToolbarSlot, composerExtraActionButton, showModelSelector = true, composerDisabled = false, composerDisabledPlaceholder, isNewThread, isThreadStateLoading, onSlashCommand, execMode, onExecModeChange, approvalActions, planModeDisabled, planModeDisabledReason, selectedModel, defaultModel, selectedEngine, selectedEffort, availableModels, modelListLoading, onModelChange, onEffortChange, availableAgents, selectedAgent, hostedHarness, onAgentChange, imageModelMenu, onForkChat, onConnectProvider, onConnectLocalRuntime, plusMenuMode = "full", providerStatusChecksEnabled = true, loadHistoryRepository, historyReloadKey, externalStreaming = false, agentChatSurface = "app", desktopIdentityUnauthenticated = false, desktopIdentityAuthenticated = false, onThreadRestoreNotFound, suppressInlineOpenApp = false, }, ref) {
1391
1391
  const t = useT();
1392
1392
  const thread = useThread();
1393
1393
  const threadRuntime = useThreadRuntime();
@@ -1856,6 +1856,9 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1856
1856
  setIsRestoring(true);
1857
1857
  setRestoreAttempt((attempt) => attempt + 1);
1858
1858
  }, [isNewThread, threadId]);
1859
+ const missingThreadNotifiedRef = useRef(null);
1860
+ const desktopIdentityAuthenticatedRef = useRef(desktopIdentityAuthenticated);
1861
+ const desktopIdentityRestoreRetryPendingRef = useRef(false);
1859
1862
  // The desktop identity gate and chat restore run in sibling surfaces. If the
1860
1863
  // gate wins the race after a masked 404 has already rendered, clear the
1861
1864
  // transient not-found card and leave the user at a fresh composer.
@@ -1864,7 +1867,6 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1864
1867
  return;
1865
1868
  setThreadRestoreError((current) => current === "not-found" ? null : current);
1866
1869
  }, [desktopIdentityUnauthenticated]);
1867
- const desktopIdentityAuthenticatedRef = useRef(desktopIdentityAuthenticated);
1868
1870
  useEffect(() => {
1869
1871
  const becameAuthenticated = desktopIdentityAuthenticated && !desktopIdentityAuthenticatedRef.current;
1870
1872
  desktopIdentityAuthenticatedRef.current = desktopIdentityAuthenticated;
@@ -1877,6 +1879,7 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1877
1879
  // A saved-thread request can race the identity handoff and be masked as a
1878
1880
  // 404/401/403. Retry once the host confirms the authenticated session so
1879
1881
  // the thread is restored without requiring a remount or manual retry.
1882
+ desktopIdentityRestoreRetryPendingRef.current = true;
1880
1883
  retryThreadRestore();
1881
1884
  }, [
1882
1885
  agentChatSurface,
@@ -1885,6 +1888,30 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
1885
1888
  retryThreadRestore,
1886
1889
  threadId,
1887
1890
  ]);
1891
+ useEffect(() => {
1892
+ if (threadRestoreError !== "not-found") {
1893
+ desktopIdentityRestoreRetryPendingRef.current = false;
1894
+ return;
1895
+ }
1896
+ if (!threadId ||
1897
+ !onThreadRestoreNotFound ||
1898
+ missingThreadNotifiedRef.current === threadId ||
1899
+ (agentChatSurface === "desktop" &&
1900
+ (!desktopIdentityAuthenticated ||
1901
+ desktopIdentityUnauthenticated ||
1902
+ desktopIdentityRestoreRetryPendingRef.current))) {
1903
+ return;
1904
+ }
1905
+ missingThreadNotifiedRef.current = threadId;
1906
+ onThreadRestoreNotFound();
1907
+ }, [
1908
+ agentChatSurface,
1909
+ desktopIdentityAuthenticated,
1910
+ desktopIdentityUnauthenticated,
1911
+ onThreadRestoreNotFound,
1912
+ threadId,
1913
+ threadRestoreError,
1914
+ ]);
1888
1915
  const onSaveThreadRef = useRef(onSaveThread);
1889
1916
  onSaveThreadRef.current = onSaveThread;
1890
1917
  const onGenerateTitleRef = useRef(onGenerateTitle);
@@ -1970,7 +1970,11 @@ export function MultiTabAssistantChat({ showTabBar = true, renderHeader, renderO
1970
1970
  else {
1971
1971
  chatRefs.current.delete(tabId);
1972
1972
  }
1973
- }, threadId: tabId, tabId: tabId, browserTabId: browserTabId, contextScope: scope, contextNamespace: contextNamespace, isolateHistoryByScope: isolateHistoryByScope, isActiveComposer: tabId === activeThreadId, apiUrl: apiUrl, isNewThread: newThreadIds.current.has(tabId) || isNewThread(tabId), isThreadStateLoading: isLoading, onMessageCountChange: (count) => setMessageCounts((prev) => prev[tabId] === count
1973
+ }, threadId: tabId, tabId: tabId, browserTabId: browserTabId, contextScope: scope, contextNamespace: contextNamespace, isolateHistoryByScope: isolateHistoryByScope, isActiveComposer: tabId === activeThreadId, apiUrl: apiUrl, isNewThread: newThreadIds.current.has(tabId) || isNewThread(tabId), onThreadRestoreNotFound: tabId === activeThreadId &&
1974
+ (props.agentChatSurface !== "desktop" ||
1975
+ props.desktopIdentityAuthenticated === true)
1976
+ ? clearActiveTab
1977
+ : undefined, isThreadStateLoading: isLoading, onMessageCountChange: (count) => setMessageCounts((prev) => prev[tabId] === count
1974
1978
  ? prev
1975
1979
  : { ...prev, [tabId]: count }), onSaveThread: handleSaveThread, onGenerateTitle: handleGenerateTitle, onSlashCommand: handleSlashCommand, selectedModel: modelSelection?.model, selectedEngine: modelSelection?.engine, selectedEffort: modelSelection?.effort ?? DEFAULT_REASONING_EFFORT, composerSlot: props.composerSlot, defaultModel: defaultModel, availableModels: availableModels, modelListLoading: modelListLoading, onModelChange: handleModelChange, onEffortChange: handleEffortChange, onForkChat: () => handleForkChat(tabId),
1976
1980
  // Sub-agent tabs are read-only: sending a new message from the
@@ -62,11 +62,11 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
62
62
  error: string;
63
63
  states?: undefined;
64
64
  } | {
65
+ error?: undefined;
65
66
  states: {
66
67
  clientId: number;
67
68
  state: string;
68
69
  }[];
69
- error?: undefined;
70
70
  }>>;
71
71
  /**
72
72
  * GET /_agent-native/collab/:docId/users
@@ -77,9 +77,9 @@ export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("
77
77
  error: string;
78
78
  users?: undefined;
79
79
  } | {
80
+ error?: undefined;
80
81
  users: {
81
82
  clientId: number;
82
83
  lastSeen: number;
83
84
  }[];
84
- error?: undefined;
85
85
  }>>;
@@ -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
- error: string;
30
29
  ok?: undefined;
30
+ error: string;
31
31
  } | {
32
32
  error?: undefined;
33
33
  ok: boolean;
@@ -11,14 +11,14 @@
11
11
  * DELETE /_agent-native/notifications/:id — delete
12
12
  */
13
13
  export declare function createNotificationsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<"" | import("./types.js").Notification[] | {
14
+ error?: undefined;
14
15
  count: number;
15
16
  updated?: undefined;
16
- error?: undefined;
17
17
  ok?: undefined;
18
18
  } | {
19
+ error?: undefined;
19
20
  count?: undefined;
20
21
  updated: number;
21
- error?: undefined;
22
22
  ok?: undefined;
23
23
  } | {
24
24
  count?: undefined;
@@ -26,8 +26,8 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
26
26
  error: string;
27
27
  ok?: undefined;
28
28
  } | {
29
+ error?: undefined;
29
30
  count?: undefined;
30
31
  updated?: undefined;
31
- error?: undefined;
32
32
  ok: boolean;
33
33
  }>>;
@@ -41,16 +41,16 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
41
41
  thumbsUpRate: number;
42
42
  avgEvalScore: number;
43
43
  } | {
44
+ error?: undefined;
44
45
  summary: import("./types.js").TraceSummary;
45
46
  spans: import("./types.js").TraceSpan[];
46
47
  id?: undefined;
47
- error?: undefined;
48
48
  ok?: undefined;
49
49
  } | {
50
+ error?: undefined;
50
51
  summary?: undefined;
51
52
  spans?: undefined;
52
53
  id: string;
53
- error?: undefined;
54
54
  ok?: undefined;
55
55
  } | {
56
56
  summary?: undefined;
@@ -59,9 +59,9 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
59
59
  error: any;
60
60
  ok?: undefined;
61
61
  } | {
62
+ error?: undefined;
62
63
  summary?: undefined;
63
64
  spans?: undefined;
64
65
  id?: undefined;
65
- error?: undefined;
66
66
  ok: boolean;
67
67
  }>>;
@@ -15,6 +15,6 @@ export declare function createProgressHandler(): import("h3").EventHandlerWithFe
15
15
  error: string;
16
16
  ok?: undefined;
17
17
  } | {
18
- ok: boolean;
19
18
  error?: undefined;
19
+ ok: boolean;
20
20
  }>>;
@@ -75,7 +75,6 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
75
75
  user: "user";
76
76
  }>>;
77
77
  }, z.core.$strip>>, {
78
- message?: undefined;
79
78
  deleted?: undefined;
80
79
  found?: undefined;
81
80
  id?: undefined;
@@ -92,8 +91,8 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
92
91
  provider?: undefined;
93
92
  registered?: undefined;
94
93
  label?: undefined;
95
- } | {
96
94
  message?: undefined;
95
+ } | {
97
96
  deleted?: undefined;
98
97
  id?: undefined;
99
98
  providers?: undefined;
@@ -102,8 +101,8 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
102
101
  provider: import("../custom-registry.js").CustomProviderConfig;
103
102
  registered?: undefined;
104
103
  label?: undefined;
105
- } | {
106
104
  message?: undefined;
105
+ } | {
107
106
  deleted?: undefined;
108
107
  providers?: undefined;
109
108
  count?: undefined;
@@ -112,8 +111,8 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
112
111
  id: string;
113
112
  registered?: undefined;
114
113
  label?: undefined;
115
- } | {
116
114
  message?: undefined;
115
+ } | {
117
116
  found?: undefined;
118
117
  providers?: undefined;
119
118
  count?: undefined;
@@ -122,6 +121,7 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
122
121
  id: string;
123
122
  registered?: undefined;
124
123
  label?: undefined;
124
+ message?: undefined;
125
125
  } | {
126
126
  deleted?: undefined;
127
127
  found?: undefined;
@@ -330,39 +330,39 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
330
330
  label?: undefined;
331
331
  } | {
332
332
  id?: undefined;
333
- count?: undefined;
334
333
  deleted?: undefined;
335
334
  message?: undefined;
336
335
  providers?: undefined;
336
+ count?: undefined;
337
337
  found: boolean;
338
338
  provider: import("../custom-registry.js").CustomProviderConfig;
339
339
  registered?: undefined;
340
340
  label?: undefined;
341
341
  } | {
342
- count?: undefined;
343
342
  deleted?: undefined;
344
343
  message?: undefined;
345
344
  providers?: undefined;
345
+ count?: undefined;
346
346
  provider?: undefined;
347
347
  found: boolean;
348
348
  id: string;
349
349
  registered?: undefined;
350
350
  label?: undefined;
351
351
  } | {
352
- count?: undefined;
353
352
  found?: undefined;
354
353
  message?: undefined;
355
354
  providers?: undefined;
355
+ count?: undefined;
356
356
  provider?: undefined;
357
357
  deleted: boolean;
358
358
  id: string;
359
359
  registered?: undefined;
360
360
  label?: undefined;
361
361
  } | {
362
- count?: undefined;
363
362
  deleted?: undefined;
364
363
  found?: undefined;
365
364
  providers?: undefined;
365
+ count?: undefined;
366
366
  provider?: undefined;
367
367
  registered: boolean;
368
368
  id: string;
@@ -34,16 +34,16 @@ export declare function createListSecretsHandler(): import("h3").EventHandlerWit
34
34
  /** POST /_agent-native/secrets/:key — write a secret. */
35
35
  export declare function createWriteSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
36
36
  error: string;
37
- status?: undefined;
38
37
  ok?: undefined;
38
+ status?: undefined;
39
39
  } | {
40
40
  ok: boolean;
41
41
  status: string;
42
42
  error?: undefined;
43
43
  } | {
44
+ ok?: undefined;
44
45
  error: string;
45
46
  removed?: undefined;
46
- ok?: undefined;
47
47
  } | {
48
48
  ok: boolean;
49
49
  removed: boolean;
@@ -54,9 +54,9 @@ export declare function createWriteSecretHandler(): import("h3").EventHandlerWit
54
54
  * or the current stored value without changing anything.
55
55
  */
56
56
  export declare function createTestSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
57
+ ok?: undefined;
57
58
  error: string;
58
59
  note?: undefined;
59
- ok?: undefined;
60
60
  } | {
61
61
  ok: boolean;
62
62
  note?: undefined;
@@ -2277,8 +2277,14 @@ function createAuthGuardFn(app) {
2277
2277
  // health exposes only aggregate readiness and a trivial `SELECT 1`.
2278
2278
  // Without this bypass the gate below 401s anonymous /_agent-native/*
2279
2279
  // requests before either probe can run.
2280
- if (p === "/_agent-native/ping" || p === "/_agent-native/health")
2280
+ if (p === "/_agent-native/ping" ||
2281
+ p === "/_agent-native/health" ||
2282
+ // The credential self-check is read by an unauthenticated monitor. Without
2283
+ // this the gate 401s it, the monitor reads a non-JSON body as "route not
2284
+ // deployed", and the check silently never runs.
2285
+ p === "/_agent-native/health/google") {
2281
2286
  return;
2287
+ }
2282
2288
  if (getMethod(event) === "GET" && p.startsWith("/_agent-native/avatar/")) {
2283
2289
  return;
2284
2290
  }
@@ -31,7 +31,7 @@ import { resolveAuthCookieNamespace } from "./cookie-namespace.js";
31
31
  import { getWorkspaceA2ADerivedSecret } from "./derived-secret.js";
32
32
  import { renderMagicLinkEmail, renderResetPasswordEmail, renderVerifySignupEmail, } from "./email-templates.js";
33
33
  import { getEmailReadiness, sendEmail } from "./email.js";
34
- import { resolveGoogleSignInCredentials } from "./google-oauth-credentials.js";
34
+ import { recordActiveGoogleSignInCredentials, resolveGoogleSignInCredentials, } from "./google-oauth-credentials.js";
35
35
  import { readMagicLinkSignupAttribution } from "./magic-link-attribution.js";
36
36
  import { getRequestContext, hasContinuationLocalRequestContext, } from "./request-context.js";
37
37
  export { getAuthLoginMode, resolveAuthLoginMode, resolveAuthLoginModeFromReadiness, } from "./auth-login-mode.js";
@@ -865,14 +865,28 @@ async function createBetterAuthInstance(config) {
865
865
  ...config?.socialProviders,
866
866
  };
867
867
  const extraScopes = config?.googleScopes ?? [];
868
+ const configuredGoogleProvider = typeof config?.socialProviders?.google === "function"
869
+ ? await config.socialProviders.google()
870
+ : config?.socialProviders?.google;
871
+ const configuredGoogleCredentials = configuredGoogleProvider &&
872
+ typeof configuredGoogleProvider.clientId === "string" &&
873
+ typeof configuredGoogleProvider.clientSecret === "string"
874
+ ? {
875
+ clientId: configuredGoogleProvider.clientId,
876
+ clientSecret: configuredGoogleProvider.clientSecret,
877
+ }
878
+ : null;
868
879
  const googleCredentials = extraScopes.length > 0
869
880
  ? process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET
870
881
  ? {
871
882
  clientId: process.env.GOOGLE_CLIENT_ID,
872
883
  clientSecret: process.env.GOOGLE_CLIENT_SECRET,
873
884
  }
874
- : null
875
- : resolveGoogleSignInCredentials();
885
+ : configuredGoogleCredentials
886
+ : (resolveGoogleSignInCredentials() ?? configuredGoogleCredentials);
887
+ // Publish the pair actually wired to the provider so the credential
888
+ // self-check probes what the callback uses, not what it would prefer.
889
+ recordActiveGoogleSignInCredentials(googleCredentials);
876
890
  if (googleCredentials) {
877
891
  // When the template requests broader scopes (Gmail, Calendar, etc.)
878
892
  // ask for them on the primary sign-in flow so a separate "Connect
@@ -884,6 +898,7 @@ async function createBetterAuthInstance(config) {
884
898
  const baseScopes = ["openid", "email", "profile"];
885
899
  const mergedScopes = Array.from(new Set([...baseScopes, ...extraScopes]));
886
900
  socialProviders.google = {
901
+ ...(configuredGoogleProvider ?? {}),
887
902
  clientId: googleCredentials.clientId,
888
903
  clientSecret: googleCredentials.clientSecret,
889
904
  ...(extraScopes.length > 0
@@ -53,6 +53,7 @@ import { createEmbedStartRouteHandler } from "./embed-route.js";
53
53
  import { shouldReportError } from "./error-noise-filter.js";
54
54
  import { FRAMEWORK_AUTH_EARLY_PATHS, getH3App, awaitBootstrap, markDefaultPluginProvided, markFrameworkRoutesReadyBeforeBootstrap, trackPluginInit, } from "./framework-request-handler.js";
55
55
  import { createGatewayAccessCheckHandler } from "./gateway-access-check.js";
56
+ import { checkGoogleSignInCredential } from "./google-credential-check.js";
56
57
  import { getAppBasePath, getOrigin } from "./google-oauth.js";
57
58
  import { createGoogleRealtimeSessionHandler } from "./google-realtime-session.js";
58
59
  import { readBody, DEFAULT_UPLOAD_MAX_FILE_BYTES, isAllowedUploadMimeType, } from "./h3-helpers.js";
@@ -1002,6 +1003,17 @@ export function createCoreRoutesPlugin(options = {}) {
1002
1003
  }));
1003
1004
  }
1004
1005
  if (!options.disableHealth) {
1006
+ // Registered before `/health` because h3 matches by prefix, and the
1007
+ // health handler would otherwise swallow this path.
1008
+ getH3App(nitroApp).use(`${P}/health/google`, defineEventHandler(async (event) => {
1009
+ setResponseHeader(event, "cache-control", "no-store");
1010
+ const result = await checkGoogleSignInCredential();
1011
+ // `invalid` is the fleet-wide outage shape: the deploy is up and
1012
+ // healthy while nobody can sign in. Page on it.
1013
+ if (result.status === "invalid")
1014
+ setResponseStatus(event, 503);
1015
+ return result;
1016
+ }));
1005
1017
  getH3App(nitroApp).use(`${P}/health`, defineEventHandler(async (event) => {
1006
1018
  setResponseHeader(event, "cache-control", "no-store");
1007
1019
  const schema = event.url?.searchParams.get("schema") === "1" ||
@@ -0,0 +1,38 @@
1
+ export type GoogleCredentialStatus =
2
+ /** Google accepted the client id and secret. */
3
+ "valid"
4
+ /** Google rejected the client id or secret — sign-in is broken. */
5
+ | "invalid"
6
+ /** No sign-in credentials are configured on this deploy. */
7
+ | "unconfigured"
8
+ /** Google could not be reached, or answered something unrecognised. */
9
+ | "unknown";
10
+ export interface GoogleCredentialCheck {
11
+ status: GoogleCredentialStatus;
12
+ clientId: string | null;
13
+ /** Both credential pairs are set to different Google clients. */
14
+ mismatchedPairs: boolean;
15
+ /**
16
+ * Where the probed pair came from. `active` is the pair Better Auth wired to
17
+ * the provider; `preferred` means auth had not initialised yet and this fell
18
+ * back to the preferred pair, which a scoped template may not use.
19
+ */
20
+ credentialSource: "active" | "preferred";
21
+ /** Google's `error` field, or the transport failure, when there was one. */
22
+ reason: string | null;
23
+ checkedAt: number;
24
+ }
25
+ /** Test seam: drop the memoised result. */
26
+ export declare function resetGoogleCredentialCheckCache(): void;
27
+ /**
28
+ * Ask Google whether this deploy's sign-in credentials still authenticate.
29
+ *
30
+ * The app's own callback collapses every Google failure into one error page,
31
+ * so a wrong secret is invisible from outside. This is the signal a monitor
32
+ * can read: it needs no browser, no consent grant, and no access to the secret
33
+ * beyond the process that already holds it.
34
+ */
35
+ export declare function checkGoogleSignInCredential(options?: {
36
+ ttlMs?: number;
37
+ now?: () => number;
38
+ }): Promise<GoogleCredentialCheck>;
@@ -0,0 +1,115 @@
1
+ import { describeGoogleSignInCredentialPairs, getActiveGoogleSignInCredentials, resolveGoogleSignInCredentials, } from "./google-oauth-credentials.js";
2
+ const GOOGLE_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
3
+ /**
4
+ * Google authenticates the client before it looks at the code, so a
5
+ * deliberately invalid code separates the two failures: `invalid_client` means
6
+ * the secret is wrong, `invalid_grant` means the secret authenticated and only
7
+ * the code was rejected. The redirect_uri is never reached and is a constant.
8
+ */
9
+ const PROBE_CODE = "agent-native-credential-probe";
10
+ const PROBE_REDIRECT_URI = "https://example.com/agent-native-credential-probe";
11
+ const DEFAULT_TTL_MS = 5 * 60 * 1000;
12
+ const REQUEST_TIMEOUT_MS = 10_000;
13
+ let cached = null;
14
+ /** Test seam: drop the memoised result. */
15
+ export function resetGoogleCredentialCheckCache() {
16
+ cached = null;
17
+ }
18
+ async function probeGoogle(clientId, clientSecret) {
19
+ let response;
20
+ try {
21
+ response = await fetch(GOOGLE_TOKEN_ENDPOINT, {
22
+ method: "POST",
23
+ headers: { "content-type": "application/x-www-form-urlencoded" },
24
+ body: new URLSearchParams({
25
+ grant_type: "authorization_code",
26
+ code: PROBE_CODE,
27
+ client_id: clientId,
28
+ client_secret: clientSecret,
29
+ redirect_uri: PROBE_REDIRECT_URI,
30
+ }),
31
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
32
+ });
33
+ }
34
+ catch (error) {
35
+ // Unreachable is not the same as wrong. Reporting "valid" here would
36
+ // recreate the exact blind spot this check exists to remove.
37
+ return {
38
+ status: "unknown",
39
+ reason: error instanceof Error ? error.message : "fetch failed",
40
+ };
41
+ }
42
+ let error = null;
43
+ try {
44
+ const body = (await response.json());
45
+ if (typeof body?.error === "string")
46
+ error = body.error;
47
+ }
48
+ catch {
49
+ error = null;
50
+ }
51
+ if (error === "invalid_grant")
52
+ return { status: "valid", reason: error };
53
+ if (error === "invalid_client")
54
+ return { status: "invalid", reason: error };
55
+ if (response.ok) {
56
+ // A probe code must never mint a token. Something is not the API we think.
57
+ return { status: "unknown", reason: "unexpected token grant" };
58
+ }
59
+ return { status: "unknown", reason: error ?? `http ${response.status}` };
60
+ }
61
+ /**
62
+ * Ask Google whether this deploy's sign-in credentials still authenticate.
63
+ *
64
+ * The app's own callback collapses every Google failure into one error page,
65
+ * so a wrong secret is invisible from outside. This is the signal a monitor
66
+ * can read: it needs no browser, no consent grant, and no access to the secret
67
+ * beyond the process that already holds it.
68
+ */
69
+ export async function checkGoogleSignInCredential(options) {
70
+ const now = options?.now ?? Date.now;
71
+ const ttlMs = options?.ttlMs ?? DEFAULT_TTL_MS;
72
+ const at = now();
73
+ // Prefer what Better Auth actually wired up. A template requesting broader
74
+ // scopes runs on GOOGLE_CLIENT_*, so re-deriving the preferred pair here
75
+ // would test a credential the callback never touches.
76
+ const active = getActiveGoogleSignInCredentials();
77
+ if (cached &&
78
+ cached.expiresAt > at &&
79
+ cached.activeCredentialsVersion === active.version) {
80
+ return cached.value;
81
+ }
82
+ const pairs = describeGoogleSignInCredentialPairs();
83
+ const credentials = active.recorded
84
+ ? active.credentials
85
+ : resolveGoogleSignInCredentials();
86
+ const credentialSource = active.recorded
87
+ ? "active"
88
+ : "preferred";
89
+ const value = credentials
90
+ ? {
91
+ ...(await probeGoogle(credentials.clientId, credentials.clientSecret)),
92
+ clientId: credentials.clientId,
93
+ mismatchedPairs: pairs.mismatched,
94
+ credentialSource,
95
+ checkedAt: at,
96
+ }
97
+ : {
98
+ status: "unconfigured",
99
+ clientId: null,
100
+ mismatchedPairs: pairs.mismatched,
101
+ credentialSource,
102
+ reason: null,
103
+ checkedAt: at,
104
+ };
105
+ // Only memoise answers Google actually gave. Caching a transport failure for
106
+ // five minutes would hide a recovery for five minutes.
107
+ if (value.status !== "unknown") {
108
+ cached = {
109
+ value,
110
+ expiresAt: at + ttlMs,
111
+ activeCredentialsVersion: active.version,
112
+ };
113
+ }
114
+ return value;
115
+ }
@@ -44,6 +44,34 @@ export declare function resolveGoogleProviderCredentialCandidatesWithReader(opti
44
44
  */
45
45
  export declare function resolveGoogleSignInCredentials(): GoogleOAuthCredentials | null;
46
46
  export declare function hasGoogleSignInCredentials(): boolean;
47
+ /**
48
+ * Record the pair Better Auth actually handed to the Google provider.
49
+ *
50
+ * The effective pair is not always the preferred one: a template asking for
51
+ * broader scopes is wired to GOOGLE_CLIENT_ID/SECRET instead. Anything testing
52
+ * "the credential the callback will use" must read this rather than
53
+ * re-deriving it, or it will verify a pair nothing reads and report healthy.
54
+ */
55
+ export declare function recordActiveGoogleSignInCredentials(credentials: GoogleOAuthCredentials | null): void;
56
+ /** Test seam: forget what Better Auth wired, as if it had not initialised. */
57
+ export declare function resetActiveGoogleSignInCredentials(): void;
58
+ export declare function getActiveGoogleSignInCredentials(): {
59
+ credentials: GoogleOAuthCredentials | null;
60
+ recorded: boolean;
61
+ version: number;
62
+ };
63
+ /**
64
+ * Which sign-in credential pairs are configured, and whether they disagree.
65
+ *
66
+ * `mismatched` is the state that hid the 2026-08-20 outage: two pairs naming
67
+ * different Google clients, where only the winner is ever read. Callers report
68
+ * it; the resolver only warns.
69
+ */
70
+ export declare function describeGoogleSignInCredentialPairs(): {
71
+ signInClientId: string | null;
72
+ providerClientId: string | null;
73
+ mismatched: boolean;
74
+ };
47
75
  export declare function resolveGoogleProviderCredentials(): GoogleOAuthCredentials | null;
48
76
  export declare function resolveGoogleLegacyProviderCredentials(): GoogleOAuthCredentials | null;
49
77
  export declare function resolveGoogleProviderCredentialCandidates(): GoogleOAuthCredentials[];
@@ -75,6 +75,51 @@ export function resolveGoogleSignInCredentials() {
75
75
  export function hasGoogleSignInCredentials() {
76
76
  return resolveGoogleSignInCredentials() !== null;
77
77
  }
78
+ let activeSignInCredentials = null;
79
+ let activeSignInCredentialsRecorded = false;
80
+ let activeSignInCredentialsVersion = 0;
81
+ /**
82
+ * Record the pair Better Auth actually handed to the Google provider.
83
+ *
84
+ * The effective pair is not always the preferred one: a template asking for
85
+ * broader scopes is wired to GOOGLE_CLIENT_ID/SECRET instead. Anything testing
86
+ * "the credential the callback will use" must read this rather than
87
+ * re-deriving it, or it will verify a pair nothing reads and report healthy.
88
+ */
89
+ export function recordActiveGoogleSignInCredentials(credentials) {
90
+ activeSignInCredentials = credentials;
91
+ activeSignInCredentialsRecorded = true;
92
+ activeSignInCredentialsVersion += 1;
93
+ }
94
+ /** Test seam: forget what Better Auth wired, as if it had not initialised. */
95
+ export function resetActiveGoogleSignInCredentials() {
96
+ activeSignInCredentials = null;
97
+ activeSignInCredentialsRecorded = false;
98
+ activeSignInCredentialsVersion += 1;
99
+ }
100
+ export function getActiveGoogleSignInCredentials() {
101
+ return {
102
+ credentials: activeSignInCredentials,
103
+ recorded: activeSignInCredentialsRecorded,
104
+ version: activeSignInCredentialsVersion,
105
+ };
106
+ }
107
+ /**
108
+ * Which sign-in credential pairs are configured, and whether they disagree.
109
+ *
110
+ * `mismatched` is the state that hid the 2026-08-20 outage: two pairs naming
111
+ * different Google clients, where only the winner is ever read. Callers report
112
+ * it; the resolver only warns.
113
+ */
114
+ export function describeGoogleSignInCredentialPairs() {
115
+ const signIn = readCredentialPair("GOOGLE_SIGN_IN_CLIENT_ID", "GOOGLE_SIGN_IN_CLIENT_SECRET");
116
+ const provider = readCredentialPair("GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET");
117
+ return {
118
+ signInClientId: signIn?.clientId ?? null,
119
+ providerClientId: provider?.clientId ?? null,
120
+ mismatched: Boolean(signIn && provider && signIn.clientId !== provider.clientId),
121
+ };
122
+ }
78
123
  export function resolveGoogleProviderCredentials() {
79
124
  return readCredentialPair(GOOGLE_PRIMARY_PROVIDER_CREDENTIAL_KEYS.clientIdKey, GOOGLE_PRIMARY_PROVIDER_CREDENTIAL_KEYS.clientSecretKey);
80
125
  }
@@ -26,8 +26,8 @@ export declare function createRealtimeTokenHandler(): import("h3").EventHandlerW
26
26
  expiresAt?: undefined;
27
27
  ttlSeconds?: undefined;
28
28
  } | {
29
+ error?: undefined;
29
30
  token: string;
30
31
  expiresAt: string;
31
32
  ttlSeconds: number;
32
- error?: undefined;
33
33
  }>>;
@@ -20,6 +20,6 @@ export declare function createTranscribeVoiceHandler(): import("h3").EventHandle
20
20
  error: string;
21
21
  text?: undefined;
22
22
  } | {
23
- text: string;
24
23
  error?: undefined;
24
+ text: string;
25
25
  }>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.168.3",
3
+ "version": "0.168.5",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {