@agent-native/core 0.168.2 → 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.
- package/corpus/templates/analytics/server/lib/dashboards-store.ts +32 -5
- package/corpus/templates/analytics/server/lib/scoped-settings.ts +2 -3
- package/corpus/templates/calendar/app/root.tsx +2 -0
- package/corpus/templates/clips/desktop/src/app.tsx +4 -8
- package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.get.ts +17 -11
- package/corpus/templates/clips/server/routes/_agent-native/google/callback.get.ts +6 -5
- package/corpus/templates/design/app/root.tsx +5 -1
- package/dist/app-config/store.d.ts +8 -0
- package/dist/app-config/store.js +19 -1
- package/dist/client/AssistantChat.d.ts +2 -0
- package/dist/client/AssistantChat.js +29 -2
- package/dist/client/MultiTabAssistantChat.js +5 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
- package/dist/provider-api/actions/provider-api.d.ts +4 -4
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/auth.js +7 -1
- package/dist/server/better-auth-instance.js +18 -3
- package/dist/server/core-routes-plugin.js +12 -0
- package/dist/server/google-credential-check.d.ts +38 -0
- package/dist/server/google-credential-check.js +115 -0
- package/dist/server/google-oauth-credentials.d.ts +28 -0
- package/dist/server/google-oauth-credentials.js +45 -0
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/settings/index.d.ts +1 -1
- package/dist/settings/index.js +1 -1
- package/dist/settings/org-settings.js +7 -3
- package/package.json +3 -3
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
import { isPostgres } from "@agent-native/core/db";
|
|
16
16
|
import { recordChange } from "@agent-native/core/server";
|
|
17
17
|
import {
|
|
18
|
-
getAllSettings,
|
|
19
18
|
getOrgSetting,
|
|
20
19
|
getUserSetting,
|
|
21
20
|
deleteOrgSetting,
|
|
22
21
|
deleteUserSetting,
|
|
22
|
+
listSettingsByPrefix,
|
|
23
23
|
} from "@agent-native/core/settings";
|
|
24
24
|
import {
|
|
25
25
|
accessFilter,
|
|
@@ -167,6 +167,33 @@ interface AccessCtx {
|
|
|
167
167
|
orgId: string | null;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Legacy KV rows are only ever reachable under `o:<orgId>:` or `u:<email>:`,
|
|
172
|
+
* so a caller can match nothing else. Reading them with `getAllSettings()`
|
|
173
|
+
* pulled and JSON-parsed every tenant's settings row into the Lambda to
|
|
174
|
+
* string-match those two prefixes, putting the whole deployment's settings
|
|
175
|
+
* table on the critical path of every dashboard and analysis list read.
|
|
176
|
+
*/
|
|
177
|
+
async function getScopedLegacySettings(
|
|
178
|
+
ctx: Pick<AccessCtx, "email" | "orgId">,
|
|
179
|
+
): Promise<Record<string, Record<string, unknown>>> {
|
|
180
|
+
// User scope first, then org: callers append these to the SQL rows in
|
|
181
|
+
// iteration order and never re-sort, so the order is user-visible. The
|
|
182
|
+
// previous full-table read inherited whatever order the settings table
|
|
183
|
+
// returned, which no query pinned.
|
|
184
|
+
const prefixes: string[] = [];
|
|
185
|
+
if (ctx.email) prefixes.push(`u:${ctx.email}:`);
|
|
186
|
+
if (ctx.orgId) prefixes.push(`o:${ctx.orgId}:`);
|
|
187
|
+
if (prefixes.length === 0) return {};
|
|
188
|
+
const scoped: Record<string, Record<string, unknown>> = {};
|
|
189
|
+
for (const entries of await Promise.all(
|
|
190
|
+
prefixes.map((prefix) => listSettingsByPrefix(prefix)),
|
|
191
|
+
)) {
|
|
192
|
+
for (const { key, value } of entries) scoped[key] = value;
|
|
193
|
+
}
|
|
194
|
+
return scoped;
|
|
195
|
+
}
|
|
196
|
+
|
|
170
197
|
const SQL_PREFIX = "sql-dashboard-";
|
|
171
198
|
const EXPLORER_PREFIX = "dashboard-";
|
|
172
199
|
|
|
@@ -666,7 +693,7 @@ export async function listDashboards(
|
|
|
666
693
|
// entirely when the caller wants archived-only or hidden-only records.
|
|
667
694
|
if (archived === "archived" || hidden === "hidden") return out;
|
|
668
695
|
try {
|
|
669
|
-
const all = await
|
|
696
|
+
const all = await getScopedLegacySettings(ctx);
|
|
670
697
|
for (const [key, value] of Object.entries(all)) {
|
|
671
698
|
let id: string | null = null;
|
|
672
699
|
let kind: DashboardKind | null = null;
|
|
@@ -814,7 +841,7 @@ export async function listDashboardSummaries(
|
|
|
814
841
|
|
|
815
842
|
if (archived === "archived" || hidden === "hidden") return out;
|
|
816
843
|
try {
|
|
817
|
-
const all = await
|
|
844
|
+
const all = await getScopedLegacySettings(ctx);
|
|
818
845
|
for (const [key, value] of Object.entries(all)) {
|
|
819
846
|
let id: string | null = null;
|
|
820
847
|
let kind: DashboardKind | null = null;
|
|
@@ -962,7 +989,7 @@ export async function searchDashboardReferences(
|
|
|
962
989
|
const seen = new Set(
|
|
963
990
|
ranked.map(({ record }) => `${record.kind}:${record.id}`),
|
|
964
991
|
);
|
|
965
|
-
const allSettings = await
|
|
992
|
+
const allSettings = await getScopedLegacySettings(ctx);
|
|
966
993
|
for (const [key, value] of Object.entries(allSettings)) {
|
|
967
994
|
const scope = legacyDashboardReferenceScope(key, ctx);
|
|
968
995
|
if (
|
|
@@ -2040,7 +2067,7 @@ export async function listAnalyses(
|
|
|
2040
2067
|
// the legacy scan entirely.
|
|
2041
2068
|
if (hidden === "hidden") return out;
|
|
2042
2069
|
try {
|
|
2043
|
-
const all = await
|
|
2070
|
+
const all = await getScopedLegacySettings(ctx);
|
|
2044
2071
|
for (const [key, value] of Object.entries(all)) {
|
|
2045
2072
|
let id: string | null = null;
|
|
2046
2073
|
let ownerEmail = ctx.email;
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
getOrgSetting,
|
|
12
12
|
getUserSetting,
|
|
13
13
|
listOrgSettings,
|
|
14
|
+
listSettingsByPrefix,
|
|
14
15
|
putOrgSetting,
|
|
15
16
|
putUserSetting,
|
|
16
17
|
} from "@agent-native/core/settings";
|
|
@@ -35,11 +36,9 @@ async function listUserSettings(
|
|
|
35
36
|
email: string,
|
|
36
37
|
prefix: string,
|
|
37
38
|
): Promise<Record<string, Record<string, unknown>>> {
|
|
38
|
-
const all = await getAllSettings();
|
|
39
39
|
const scopedPrefix = `${userPrefix(email)}${prefix}`;
|
|
40
40
|
const out: Record<string, Record<string, unknown>> = {};
|
|
41
|
-
for (const
|
|
42
|
-
if (!key.startsWith(scopedPrefix)) continue;
|
|
41
|
+
for (const { key, value } of await listSettingsByPrefix(scopedPrefix)) {
|
|
43
42
|
out[key.slice(userPrefix(email).length)] = value;
|
|
44
43
|
}
|
|
45
44
|
return out;
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
createAgentNativeQueryClient,
|
|
6
6
|
useDbSync,
|
|
7
7
|
} from "@agent-native/core/client/hooks";
|
|
8
|
+
import { isEmbedAuthActive } from "@agent-native/core/client/host";
|
|
8
9
|
import {
|
|
9
10
|
getLocaleInitScript,
|
|
10
11
|
type LocaleCode,
|
|
@@ -278,6 +279,7 @@ export default function Root() {
|
|
|
278
279
|
<AppProviders
|
|
279
280
|
queryClient={queryClient}
|
|
280
281
|
isPublicPath={isPublicPath}
|
|
282
|
+
sessionBypass={isEmbedAuthActive()}
|
|
281
283
|
clientOnlyFallback={<DefaultSpinner />}
|
|
282
284
|
toaster={<Toaster richColors position="bottom-center" />}
|
|
283
285
|
i18n={{
|
|
@@ -67,7 +67,6 @@ import {
|
|
|
67
67
|
startBubbleWebrtc,
|
|
68
68
|
type BubbleWebrtcHandle,
|
|
69
69
|
} from "./lib/bubble-webrtc";
|
|
70
|
-
import { openBoundOAuthWindow } from "./lib/desktop-oauth-window";
|
|
71
70
|
import {
|
|
72
71
|
getCameraStreamWithFallback,
|
|
73
72
|
isMediaConstraintFailure,
|
|
@@ -2127,17 +2126,15 @@ export function App() {
|
|
|
2127
2126
|
void tick();
|
|
2128
2127
|
}
|
|
2129
2128
|
|
|
2130
|
-
// Google verification
|
|
2131
|
-
//
|
|
2132
|
-
//
|
|
2129
|
+
// Google verification opens in the system browser so the user's Google
|
|
2130
|
+
// cookies are available. The client-held verifier still gates the exchange
|
|
2131
|
+
// back into this Tauri app.
|
|
2133
2132
|
async function signInExternal() {
|
|
2134
2133
|
if (signInInflightRef.current) return;
|
|
2135
2134
|
signInInflightRef.current = true;
|
|
2136
|
-
let oauthWindow: ReturnType<typeof openBoundOAuthWindow> | null = null;
|
|
2137
2135
|
|
|
2138
2136
|
try {
|
|
2139
2137
|
setSignInError(null);
|
|
2140
|
-
oauthWindow = openBoundOAuthWindow();
|
|
2141
2138
|
const flowId = crypto.randomUUID?.() ?? null;
|
|
2142
2139
|
const verifier = (() => {
|
|
2143
2140
|
const randomUuid = crypto.randomUUID;
|
|
@@ -2196,12 +2193,11 @@ export function App() {
|
|
|
2196
2193
|
: "Could not start Google sign-in.";
|
|
2197
2194
|
throw new Error(message);
|
|
2198
2195
|
}
|
|
2199
|
-
|
|
2196
|
+
await openExternal(authPayload.url);
|
|
2200
2197
|
setSignInPending("google");
|
|
2201
2198
|
startDesktopAuthExchange(flowId, "google", verifier);
|
|
2202
2199
|
} catch (err) {
|
|
2203
2200
|
console.error("[clips-tray] signInExternal failed:", err);
|
|
2204
|
-
oauthWindow?.close();
|
|
2205
2201
|
signInInflightRef.current = false;
|
|
2206
2202
|
setSignInPending(null);
|
|
2207
2203
|
setSignInError(
|
|
@@ -64,6 +64,7 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
64
64
|
}
|
|
65
65
|
const calendarConnect =
|
|
66
66
|
q.calendar === "1" || q.calendar === "true" || q.product === "calendar";
|
|
67
|
+
const desktopWebview = desktop && q.webview === "1" && !calendarConnect;
|
|
67
68
|
let desktopVerifierHash: string | undefined;
|
|
68
69
|
let desktopBrowserBindingHash: string | undefined;
|
|
69
70
|
if (flowId && !calendarConnect) {
|
|
@@ -77,12 +78,20 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
77
78
|
return { error: "Invalid desktop exchange challenge." };
|
|
78
79
|
}
|
|
79
80
|
try {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
// The system-browser flow deliberately uses the user's existing
|
|
82
|
+
// Google cookies. The client-held verifier still gates the exchange
|
|
83
|
+
// token returned to the initiating Tauri app. Only an in-app WebView
|
|
84
|
+
// needs the additional browser-partition binding.
|
|
85
|
+
if (desktopWebview) {
|
|
86
|
+
desktopBrowserBindingHash = prepareDesktopOAuthBrowserBinding(event);
|
|
87
|
+
desktopVerifierHash = await registerDesktopExchange(
|
|
88
|
+
flowId,
|
|
89
|
+
verifier,
|
|
90
|
+
desktopBrowserBindingHash,
|
|
91
|
+
);
|
|
92
|
+
} else {
|
|
93
|
+
desktopVerifierHash = await registerDesktopExchange(flowId, verifier);
|
|
94
|
+
}
|
|
86
95
|
} catch {
|
|
87
96
|
setResponseStatus(event, 400);
|
|
88
97
|
return { error: "Invalid desktop exchange challenge." };
|
|
@@ -116,13 +125,10 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
116
125
|
redirectUri,
|
|
117
126
|
owner,
|
|
118
127
|
desktop,
|
|
119
|
-
desktopWebview
|
|
128
|
+
desktopWebview,
|
|
120
129
|
addAccount: calendarConnect,
|
|
121
130
|
app: CLIPS_GOOGLE_OAUTH_APP_ID,
|
|
122
|
-
returnUrl:
|
|
123
|
-
desktop && q.webview === "1" && !calendarConnect
|
|
124
|
-
? "/?desktop_auth=complete"
|
|
125
|
-
: returnUrl,
|
|
131
|
+
returnUrl: desktopWebview ? "/?desktop_auth=complete" : returnUrl,
|
|
126
132
|
flowId: calendarConnect ? undefined : flowId,
|
|
127
133
|
desktopVerifierHash,
|
|
128
134
|
desktopBrowserBindingHash,
|
|
@@ -35,11 +35,12 @@ async function handleGoogleSignInCallback(
|
|
|
35
35
|
if (
|
|
36
36
|
flowId &&
|
|
37
37
|
(!state.desktopVerifierHash ||
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
(state.desktopWebview &&
|
|
39
|
+
(!state.desktopBrowserBindingHash ||
|
|
40
|
+
!matchesDesktopOAuthBrowserBinding(
|
|
41
|
+
event,
|
|
42
|
+
state.desktopBrowserBindingHash,
|
|
43
|
+
))))
|
|
43
44
|
) {
|
|
44
45
|
return oauthErrorPage("Desktop OAuth browser binding is invalid.");
|
|
45
46
|
}
|
|
@@ -7,7 +7,10 @@ import {
|
|
|
7
7
|
getBrowserTabId,
|
|
8
8
|
useSession,
|
|
9
9
|
} from "@agent-native/core/client/hooks";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
isEmbedAuthActive,
|
|
12
|
+
setAgentNativeApiDisabled,
|
|
13
|
+
} from "@agent-native/core/client/host";
|
|
11
14
|
import { getLocaleInitScript, useT } from "@agent-native/core/client/i18n";
|
|
12
15
|
import {
|
|
13
16
|
CommandMenu,
|
|
@@ -230,6 +233,7 @@ export default function Root() {
|
|
|
230
233
|
<AppProviders
|
|
231
234
|
queryClient={queryClient}
|
|
232
235
|
isPublicPath={isPublicPath}
|
|
236
|
+
sessionBypass={isEmbedAuthActive()}
|
|
233
237
|
i18n={{ catalog: i18nCatalog, persistPreference: !isPublicPath }}
|
|
234
238
|
toaster={<DesignToaster />}
|
|
235
239
|
>
|
|
@@ -8,6 +8,14 @@ import { type AppConfig, type AppConfigInput } from "./schema.js";
|
|
|
8
8
|
*/
|
|
9
9
|
declare const LAYER_ORDER: readonly ["env", "legacy", "app"];
|
|
10
10
|
export type AppConfigLayer = (typeof LAYER_ORDER)[number];
|
|
11
|
+
/**
|
|
12
|
+
* Nitro embeds build-only deployment markers into direct env reads. Netlify's
|
|
13
|
+
* prebuilt beta lane has those markers while building, but does not copy them
|
|
14
|
+
* into the deployed Function environment. Keep the embedded values as the
|
|
15
|
+
* fallback for this one config boundary so a later dynamic `env[key]` read
|
|
16
|
+
* cannot erase the build decision from the server bundle.
|
|
17
|
+
*/
|
|
18
|
+
export declare function readConfigEnvironment(embedded?: Record<string, string | undefined>): Record<string, string | undefined>;
|
|
11
19
|
/**
|
|
12
20
|
* Writes one layer of the ladder. Framework-internal: the deprecated setters
|
|
13
21
|
* use it to keep working without reintroducing a second namespace.
|
package/dist/app-config/store.js
CHANGED
|
@@ -48,6 +48,24 @@ function resolve(envLayer) {
|
|
|
48
48
|
}
|
|
49
49
|
return appConfigSchema.parse(merged);
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Nitro embeds build-only deployment markers into direct env reads. Netlify's
|
|
53
|
+
* prebuilt beta lane has those markers while building, but does not copy them
|
|
54
|
+
* into the deployed Function environment. Keep the embedded values as the
|
|
55
|
+
* fallback for this one config boundary so a later dynamic `env[key]` read
|
|
56
|
+
* cannot erase the build decision from the server bundle.
|
|
57
|
+
*/
|
|
58
|
+
export function readConfigEnvironment(embedded = {
|
|
59
|
+
AGENT_NATIVE_RELEASE_MIGRATIONS: process.env.AGENT_NATIVE_RELEASE_MIGRATIONS,
|
|
60
|
+
AGENT_NATIVE_BETA_SCHEMA_OWNER: process.env.AGENT_NATIVE_BETA_SCHEMA_OWNER,
|
|
61
|
+
}) {
|
|
62
|
+
const env = { ...process.env };
|
|
63
|
+
for (const [key, value] of Object.entries(embedded)) {
|
|
64
|
+
if (!env[key] && value)
|
|
65
|
+
env[key] = value;
|
|
66
|
+
}
|
|
67
|
+
return env;
|
|
68
|
+
}
|
|
51
69
|
/**
|
|
52
70
|
* Writes one layer of the ladder. Framework-internal: the deprecated setters
|
|
53
71
|
* use it to keep working without reintroducing a second namespace.
|
|
@@ -84,7 +102,7 @@ export function defineAppConfig(config) {
|
|
|
84
102
|
* remove.
|
|
85
103
|
*/
|
|
86
104
|
export function getAppConfig() {
|
|
87
|
-
const envLayer = readEnvConfigLayer(appConfigSchema,
|
|
105
|
+
const envLayer = readEnvConfigLayer(appConfigSchema, readConfigEnvironment());
|
|
88
106
|
const signature = JSON.stringify(envLayer);
|
|
89
107
|
if (state.resolved && state.envSignature === signature)
|
|
90
108
|
return state.resolved;
|
|
@@ -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),
|
|
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
|
}>>;
|
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;
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
|
|
14
14
|
*/
|
|
15
15
|
export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
16
|
-
error: string;
|
|
17
16
|
ok?: undefined;
|
|
17
|
+
error: string;
|
|
18
18
|
} | {
|
|
19
19
|
error?: undefined;
|
|
20
20
|
ok: boolean;
|
|
@@ -17,11 +17,11 @@ declare const _default: import("../../action.js").ActionDefinition<{
|
|
|
17
17
|
id?: undefined;
|
|
18
18
|
provider?: undefined;
|
|
19
19
|
} | {
|
|
20
|
+
error?: undefined;
|
|
20
21
|
configured?: undefined;
|
|
21
22
|
connectPath?: undefined;
|
|
22
23
|
url: string;
|
|
23
24
|
id: string;
|
|
24
25
|
provider: string;
|
|
25
|
-
error?: undefined;
|
|
26
26
|
}>;
|
|
27
27
|
export default _default;
|
|
@@ -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
32
|
ok: boolean;
|
|
32
|
-
error?: undefined;
|
|
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
|
}>>;
|
|
@@ -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;
|
package/dist/secrets/routes.d.ts
CHANGED
|
@@ -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
|
}>>;
|
|
@@ -27,10 +27,10 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
|
|
|
27
27
|
export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
28
28
|
error: any;
|
|
29
29
|
} | {
|
|
30
|
+
error?: undefined;
|
|
30
31
|
ok: boolean;
|
|
31
32
|
key: string;
|
|
32
33
|
baseUrlKey?: string;
|
|
33
34
|
scope: AgentEngineApiKeyScope;
|
|
34
|
-
error?: undefined;
|
|
35
35
|
}>>;
|
|
36
36
|
export {};
|
package/dist/server/auth.js
CHANGED
|
@@ -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" ||
|
|
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
|
-
:
|
|
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
|
}>>;
|
package/dist/settings/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { getSetting, mutateSetting, putSetting, deleteSetting, deleteSettingIfValue, getAllSettings, getSettingsEmitter, type StoreWriteOptions, } from "./store.js";
|
|
1
|
+
export { getSetting, mutateSetting, putSetting, deleteSetting, deleteSettingIfValue, getAllSettings, listSettingsByPrefix, getSettingsEmitter, type StoreWriteOptions, } from "./store.js";
|
|
2
2
|
export { getSettingHandler, putSettingHandler, deleteSettingHandler, } from "./handlers.js";
|
|
3
3
|
export { readSetting, writeSetting, removeSetting } from "./script-helpers.js";
|
|
4
4
|
export { getUserSetting, putUserSetting, deleteUserSetting, } from "./user-settings.js";
|
package/dist/settings/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Store
|
|
2
|
-
export { getSetting, mutateSetting, putSetting, deleteSetting, deleteSettingIfValue, getAllSettings, getSettingsEmitter, } from "./store.js";
|
|
2
|
+
export { getSetting, mutateSetting, putSetting, deleteSetting, deleteSettingIfValue, getAllSettings, listSettingsByPrefix, getSettingsEmitter, } from "./store.js";
|
|
3
3
|
// H3 route handlers
|
|
4
4
|
export { getSettingHandler, putSettingHandler, deleteSettingHandler, } from "./handlers.js";
|
|
5
5
|
// Script helpers
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* No global fallback — each org starts with a clean slate. This
|
|
8
8
|
* prevents one org's data from leaking to another.
|
|
9
9
|
*/
|
|
10
|
-
import { getSetting, mutateSetting, putSetting, deleteSetting, deleteSettingsByPrefix,
|
|
10
|
+
import { getSetting, mutateSetting, putSetting, deleteSetting, deleteSettingsByPrefix, listSettingsByPrefix, } from "./store.js";
|
|
11
11
|
function orgKey(orgId, key) {
|
|
12
12
|
return `o:${orgId}:${key}`;
|
|
13
13
|
}
|
|
@@ -37,9 +37,13 @@ export async function deleteAllOrgSettings(orgId, options) {
|
|
|
37
37
|
* Returns a map of `<key>` (without the org prefix) to value.
|
|
38
38
|
*/
|
|
39
39
|
export async function listOrgSettings(orgId, subPrefix) {
|
|
40
|
-
|
|
40
|
+
// Narrow in SQL, then apply the same checks as before. Reading this with
|
|
41
|
+
// `getAllSettings()` pulled and JSON-parsed every org's rows into the caller
|
|
42
|
+
// to keep one org's, putting the whole deployment's settings table on the
|
|
43
|
+
// critical path of any org-scoped list read.
|
|
44
|
+
const scoped = await listSettingsByPrefix(`o:${orgId}:${subPrefix ?? ""}`);
|
|
41
45
|
const out = {};
|
|
42
|
-
for (const
|
|
46
|
+
for (const { key: fullKey, value } of scoped) {
|
|
43
47
|
const m = ORG_PREFIX_RE.exec(fullKey);
|
|
44
48
|
if (!m || m[1] !== orgId)
|
|
45
49
|
continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.168.
|
|
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/
|
|
432
|
-
"@agent-native/
|
|
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",
|