@agent-native/core 0.168.3 → 0.168.4

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.
@@ -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
  }>>;
@@ -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,37 +34,37 @@ 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
+ error?: undefined;
40
41
  ok: boolean;
41
42
  status: string;
42
- error?: undefined;
43
43
  } | {
44
+ ok?: undefined;
44
45
  error: string;
45
46
  removed?: undefined;
46
- ok?: undefined;
47
47
  } | {
48
+ error?: undefined;
48
49
  ok: boolean;
49
50
  removed: boolean;
50
- error?: undefined;
51
51
  }>>;
52
52
  /**
53
53
  * POST /_agent-native/secrets/:key/test — validate an optional candidate value
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
+ error?: undefined;
61
62
  ok: boolean;
62
63
  note?: undefined;
63
- error?: undefined;
64
64
  } | {
65
+ error?: undefined;
65
66
  ok: boolean;
66
67
  note: string;
67
- error?: undefined;
68
68
  } | {
69
69
  note?: undefined;
70
70
  ok: boolean;
@@ -95,11 +95,11 @@ export interface AdHocSecretPayload {
95
95
  export declare function createAdHocSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<AdHocSecretPayload[] | {
96
96
  error: string;
97
97
  } | {
98
+ error?: undefined;
98
99
  ok: boolean;
99
100
  key: string;
100
- error?: undefined;
101
101
  } | {
102
+ error?: undefined;
102
103
  ok: boolean;
103
104
  removed: boolean;
104
- error?: undefined;
105
105
  }>>;
@@ -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.4",
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": {
@@ -428,8 +428,8 @@
428
428
  "y-protocols": "^1.0.7",
429
429
  "yjs": "^13.6.32",
430
430
  "zod": "^4.3.6",
431
- "@agent-native/recap-cli": "0.5.7",
432
- "@agent-native/toolkit": "^0.16.9"
431
+ "@agent-native/toolkit": "^0.16.9",
432
+ "@agent-native/recap-cli": "0.5.7"
433
433
  },
434
434
  "devDependencies": {
435
435
  "@ai-sdk/anthropic": "^3.0.71",