@agent-native/core 0.81.0 → 0.81.2

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.
Files changed (47) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/AgentPanel.tsx +33 -1
  5. package/corpus/core/src/server/core-routes-plugin.ts +4 -5
  6. package/corpus/core/src/server/create-server.ts +3 -5
  7. package/corpus/core/src/server/google-oauth.ts +4 -0
  8. package/corpus/templates/calendar/actions/create-event.ts +9 -2
  9. package/corpus/templates/calendar/actions/event-action-helpers.ts +8 -2
  10. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +11 -0
  11. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +17 -0
  12. package/corpus/templates/calendar/app/hooks/use-google-auth.ts +70 -39
  13. package/corpus/templates/calendar/changelog/2026-06-30-google-calendar-connection-errors-now-stay-in-the-app-instea.md +6 -0
  14. package/corpus/templates/calendar/server/handlers/google-auth.ts +77 -21
  15. package/corpus/templates/calendar/server/lib/google-calendar.ts +19 -8
  16. package/corpus/templates/design/actions/apply-design-token-edit.ts +13 -6
  17. package/corpus/templates/design/actions/import-design-tokens.ts +17 -13
  18. package/corpus/templates/design/actions/preview-design-token-edit.ts +20 -3
  19. package/corpus/templates/design/actions/run-design-audit.ts +9 -2
  20. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +35 -14
  21. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +20 -3
  22. package/corpus/templates/design/app/pages/DesignEditor.tsx +237 -51
  23. package/corpus/templates/design/app/pages/VisualEdit.tsx +1 -1
  24. package/corpus/templates/design/changelog/2026-06-30-design-editor-undo-drag-drop-style-editing-and-visual-edit-s.md +6 -0
  25. package/corpus/templates/design/shared/resolve-tweaks.ts +29 -6
  26. package/dist/client/AgentPanel.d.ts +1 -1
  27. package/dist/client/AgentPanel.d.ts.map +1 -1
  28. package/dist/client/AgentPanel.js +18 -3
  29. package/dist/client/AgentPanel.js.map +1 -1
  30. package/dist/collab/awareness.d.ts +2 -2
  31. package/dist/collab/awareness.d.ts.map +1 -1
  32. package/dist/collab/routes.d.ts +2 -2
  33. package/dist/notifications/routes.d.ts +3 -3
  34. package/dist/progress/routes.d.ts +1 -1
  35. package/dist/resources/handlers.d.ts +3 -3
  36. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  37. package/dist/server/core-routes-plugin.js +1 -2
  38. package/dist/server/core-routes-plugin.js.map +1 -1
  39. package/dist/server/create-server.d.ts.map +1 -1
  40. package/dist/server/create-server.js +1 -2
  41. package/dist/server/create-server.js.map +1 -1
  42. package/dist/server/google-oauth.d.ts +2 -0
  43. package/dist/server/google-oauth.d.ts.map +1 -1
  44. package/dist/server/google-oauth.js +3 -0
  45. package/dist/server/google-oauth.js.map +1 -1
  46. package/dist/server/transcribe-voice.d.ts +1 -1
  47. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -28,4 +28,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
  ## Generated Counts
29
29
 
30
30
  - core files: 2043
31
- - template files: 4801
31
+ - template files: 4803
@@ -1,5 +1,17 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.81.2
4
+
5
+ ### Patch Changes
6
+
7
+ - ea1cc47: Keep OAuth state and environment status checks aligned with scoped workspace credentials.
8
+
9
+ ## 0.81.1
10
+
11
+ ### Patch Changes
12
+
13
+ - ec433c3: Add a close (X) button to the left of the Chat tab in the agent pane header that closes the agent chat, and hide the open-agent button while the agent pane is open.
14
+
3
15
  ## 0.81.0
4
16
 
5
17
  ### Minor Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.81.0",
3
+ "version": "0.81.2",
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": {
@@ -85,6 +85,8 @@ import {
85
85
  getInitialAgentSidebarOpen,
86
86
  setAgentSidebarOpenPreference,
87
87
  subscribeAgentSidebarUrlChanges,
88
+ SIDEBAR_STATE_CHANGE_EVENT,
89
+ type AgentSidebarStateChangeDetail,
88
90
  } from "./agent-sidebar-state.js";
89
91
  import { trackEvent } from "./analytics.js";
90
92
  import { agentNativePath } from "./api-path.js";
@@ -969,6 +971,22 @@ function AgentPanelInner({
969
971
  (activeMode: PanelMode) => (
970
972
  <TooltipProvider delayDuration={200}>
971
973
  <div className="flex shrink-0 items-center gap-1">
974
+ {onCollapse && (
975
+ <Tooltip>
976
+ <TooltipTrigger asChild>
977
+ <button
978
+ type="button"
979
+ onClick={onCollapse}
980
+ aria-label={t("agentPanel.collapseSidebar")}
981
+ className="flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:bg-accent/50 hover:text-foreground"
982
+ style={AGENT_PANEL_CONTROL_STYLE}
983
+ >
984
+ <IconX size={16} />
985
+ </button>
986
+ </TooltipTrigger>
987
+ <TooltipContent>{t("agentPanel.collapseSidebar")}</TooltipContent>
988
+ </Tooltip>
989
+ )}
972
990
  <Tooltip>
973
991
  <TooltipTrigger asChild>
974
992
  <button
@@ -1035,7 +1053,7 @@ function AgentPanelInner({
1035
1053
  </div>
1036
1054
  </TooltipProvider>
1037
1055
  ),
1038
- [codeAccessEnabled, codeUnavailableDescription, showCliMode, t],
1056
+ [codeAccessEnabled, codeUnavailableDescription, onCollapse, showCliMode, t],
1039
1057
  );
1040
1058
 
1041
1059
  const [headerMenuOpen, setHeaderMenuOpen] = useState(false);
@@ -3027,6 +3045,20 @@ export function focusAgentChat() {
3027
3045
  * Dispatches a custom event that AgentSidebar listens for.
3028
3046
  */
3029
3047
  export function AgentToggleButton({ className }: { className?: string }) {
3048
+ const [open, setOpen] = useState(false);
3049
+ useEffect(() => {
3050
+ const handler = (event: Event) => {
3051
+ const detail = (event as CustomEvent<AgentSidebarStateChangeDetail>)
3052
+ .detail;
3053
+ if (detail && typeof detail.open === "boolean") setOpen(detail.open);
3054
+ };
3055
+ window.addEventListener(SIDEBAR_STATE_CHANGE_EVENT, handler);
3056
+ return () =>
3057
+ window.removeEventListener(SIDEBAR_STATE_CHANGE_EVENT, handler);
3058
+ }, []);
3059
+ // Hide the open-agent button while the agent pane is open; the pane has its
3060
+ // own close button.
3061
+ if (open) return null;
3030
3062
  return (
3031
3063
  <TooltipProvider delayDuration={200}>
3032
3064
  <Tooltip>
@@ -2316,11 +2316,10 @@ export function createCoreRoutesPlugin(
2316
2316
  }
2317
2317
  return Promise.all(
2318
2318
  envKeys.map(async (cfg) => {
2319
- const configured =
2320
- Boolean(process.env[cfg.key]) ||
2321
- (await runWithRequestContext({ userEmail, orgId }, () =>
2322
- resolveSecret(cfg.key).then(Boolean),
2323
- ));
2319
+ const configured = await runWithRequestContext(
2320
+ { userEmail, orgId },
2321
+ () => resolveSecret(cfg.key).then(Boolean),
2322
+ );
2324
2323
  return {
2325
2324
  key: cfg.key,
2326
2325
  label: cfg.label,
@@ -221,11 +221,9 @@ export function createServer(
221
221
  key: cfg.key,
222
222
  label: cfg.label,
223
223
  required: cfg.required ?? false,
224
- configured:
225
- Boolean(process.env[cfg.key]) ||
226
- (await runWithRequestContext({ userEmail, orgId }, () =>
227
- resolveSecret(cfg.key).then(Boolean),
228
- )),
224
+ configured: await runWithRequestContext({ userEmail, orgId }, () =>
225
+ resolveSecret(cfg.key).then(Boolean),
226
+ ),
229
227
  ...(cfg.helpText ? { helpText: cfg.helpText } : {}),
230
228
  })),
231
229
  );
@@ -442,6 +442,7 @@ export function resolveOAuthRedirectUri(
442
442
  export interface OAuthStatePayload {
443
443
  redirectUri: string;
444
444
  owner?: string;
445
+ orgId?: string;
445
446
  desktop?: boolean;
446
447
  addAccount?: boolean;
447
448
  app?: string;
@@ -512,6 +513,7 @@ function getStateSigningKey(): string {
512
513
  export interface EncodeOAuthStateOptions {
513
514
  redirectUri: string;
514
515
  owner?: string;
516
+ orgId?: string;
515
517
  desktop?: boolean;
516
518
  addAccount?: boolean;
517
519
  app?: string;
@@ -585,6 +587,7 @@ export function encodeOAuthState(
585
587
  r: opts.redirectUri,
586
588
  };
587
589
  if (opts.owner) payload.o = opts.owner;
590
+ if (opts.orgId) payload.g = opts.orgId;
588
591
  if (opts.desktop) payload.d = true;
589
592
  if (opts.addAccount) payload.a = true;
590
593
  if (opts.app) payload.app = opts.app;
@@ -631,6 +634,7 @@ export function decodeOAuthState(
631
634
  return {
632
635
  redirectUri: parsed.r || fallbackUri,
633
636
  owner: parsed.o || undefined,
637
+ orgId: typeof parsed.g === "string" ? parsed.g : undefined,
634
638
  desktop: !!parsed.d,
635
639
  addAccount: !!parsed.a,
636
640
  app: typeof parsed.app === "string" ? parsed.app : undefined,
@@ -1,6 +1,10 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import { emit } from "@agent-native/core/event-bus";
3
- import { getRequestUserEmail, buildDeepLink } from "@agent-native/core/server";
3
+ import {
4
+ buildDeepLink,
5
+ getRequestOrgId,
6
+ getRequestUserEmail,
7
+ } from "@agent-native/core/server";
4
8
  import { z } from "zod";
5
9
 
6
10
  import { prepareZoomMeetingPatch } from "../server/lib/event-video-conferencing.js";
@@ -151,7 +155,10 @@ export default defineAction({
151
155
  // Resolve account email
152
156
  let acctEmail = email;
153
157
  if (args.accountEmail && args.accountEmail !== email) {
154
- const status = await googleCalendar.getAuthStatus(email);
158
+ const status = await googleCalendar.getAuthStatus(
159
+ email,
160
+ getRequestOrgId(),
161
+ );
155
162
  const isOwned = status.accounts.some(
156
163
  (a) => a.email === args.accountEmail,
157
164
  );
@@ -1,4 +1,7 @@
1
- import { getRequestUserEmail } from "@agent-native/core/server";
1
+ import {
2
+ getRequestOrgId,
3
+ getRequestUserEmail,
4
+ } from "@agent-native/core/server";
2
5
  import { z } from "zod";
3
6
 
4
7
  import * as googleCalendar from "../server/lib/google-calendar.js";
@@ -71,7 +74,10 @@ export async function resolveOwnedAccountEmail(
71
74
  requestedAccountEmail: string | undefined,
72
75
  ownerEmail: string,
73
76
  ): Promise<string> {
74
- const status = await googleCalendar.getAuthStatus(ownerEmail);
77
+ const status = await googleCalendar.getAuthStatus(
78
+ ownerEmail,
79
+ getRequestOrgId(),
80
+ );
75
81
  if (!requestedAccountEmail) {
76
82
  if (status.accounts.length === 1) {
77
83
  return status.accounts[0].email;
@@ -189,6 +189,17 @@ export function GoogleConnectBanner({
189
189
  }
190
190
  }, [authUrl.error, fetchStatus]);
191
191
 
192
+ useEffect(() => {
193
+ if (
194
+ desktopAuthIssue?.code !== "missing_credentials" &&
195
+ desktopAuthIssue?.error !== "missing_credentials"
196
+ ) {
197
+ return;
198
+ }
199
+ setShowWizard(true);
200
+ fetchStatus();
201
+ }, [desktopAuthIssue, fetchStatus]);
202
+
192
203
  const allConfigured =
193
204
  envStatus.length > 0 && envStatus.every((k) => k.configured);
194
205
 
@@ -44,6 +44,7 @@ import {
44
44
  } from "date-fns";
45
45
  import { useState, useEffect, useMemo } from "react";
46
46
  import { Link, useLocation, useNavigate } from "react-router";
47
+ import { toast } from "sonner";
47
48
 
48
49
  import { ThemeToggle } from "@/components/ThemeToggle";
49
50
  import { Button } from "@/components/ui/button";
@@ -322,6 +323,8 @@ function GoogleConnectSidebarButton() {
322
323
  isGoogleDesktopAuthPending,
323
324
  startDesktopGoogleAuth,
324
325
  } = useGoogleDesktopAuth({
326
+ onError: (issue) =>
327
+ toast.error(issue.message || issue.error || t("settings.googleFailed")),
325
328
  onSuccess: () => window.location.reload(),
326
329
  });
327
330
 
@@ -331,6 +334,12 @@ function GoogleConnectSidebarButton() {
331
334
  window.open(authUrl.data.url, "_blank");
332
335
  }, [wantAuthUrl, authUrl.data]);
333
336
 
337
+ useEffect(() => {
338
+ if (!authUrl.error) return;
339
+ toast.error(authUrl.error.message);
340
+ setWantAuthUrl(false);
341
+ }, [authUrl.error]);
342
+
334
343
  function handleConnect() {
335
344
  if (isDesktopGoogleAuth) {
336
345
  startDesktopGoogleAuth({ previousAccountCount: 0 });
@@ -435,6 +444,8 @@ function GoogleAccountsSection({
435
444
  isGoogleDesktopAuthPending,
436
445
  startDesktopGoogleAuth,
437
446
  } = useGoogleDesktopAuth({
447
+ onError: (issue) =>
448
+ toast.error(issue.message || issue.error || t("settings.googleFailed")),
438
449
  onSuccess: () => window.location.reload(),
439
450
  });
440
451
 
@@ -444,6 +455,12 @@ function GoogleAccountsSection({
444
455
  setWantAddAccount(false);
445
456
  }, [wantAddAccount, addAccountUrl.data]);
446
457
 
458
+ useEffect(() => {
459
+ if (!addAccountUrl.error) return;
460
+ toast.error(addAccountUrl.error.message);
461
+ setWantAddAccount(false);
462
+ }, [addAccountUrl.error]);
463
+
447
464
  function handleAddAccount() {
448
465
  if (isDesktopGoogleAuth) {
449
466
  startDesktopGoogleAuth({
@@ -45,6 +45,23 @@ function bodyError(
45
45
  `${fallback} (HTTP ${res.status})`;
46
46
  const error = new Error(message);
47
47
  (error as any).status = res.status;
48
+ if (body && typeof body === "object") {
49
+ const record = body as Record<string, unknown>;
50
+ for (const key of [
51
+ "error",
52
+ "code",
53
+ "accountId",
54
+ "existingOwner",
55
+ "attemptedOwner",
56
+ ]) {
57
+ if (typeof record[key] === "string") {
58
+ (error as any)[key] = record[key];
59
+ }
60
+ }
61
+ if (!(error as any).code && typeof record.error === "string") {
62
+ (error as any).code = record.error;
63
+ }
64
+ }
48
65
  return error;
49
66
  }
50
67
 
@@ -204,6 +221,33 @@ export function useGoogleDesktopAuth(options: DesktopAuthOptions = {}) {
204
221
  setIsPending(false);
205
222
  onError?.(issue);
206
223
  };
224
+ const authStartIssue = (err: unknown): DesktopAuthIssue => {
225
+ const source = err as Partial<DesktopAuthIssue> | undefined;
226
+ const message =
227
+ err instanceof Error
228
+ ? err.message
229
+ : "Could not start Google sign-in.";
230
+ return {
231
+ code:
232
+ typeof source?.code === "string"
233
+ ? source.code
234
+ : "desktop_auth_start_failed",
235
+ error: typeof source?.error === "string" ? source.error : undefined,
236
+ message,
237
+ accountId:
238
+ typeof source?.accountId === "string"
239
+ ? source.accountId
240
+ : undefined,
241
+ existingOwner:
242
+ typeof source?.existingOwner === "string"
243
+ ? source.existingOwner
244
+ : undefined,
245
+ attemptedOwner:
246
+ typeof source?.attemptedOwner === "string"
247
+ ? source.attemptedOwner
248
+ : undefined,
249
+ };
250
+ };
207
251
  const openAuthUrl = (url: string) => {
208
252
  if (popup && !popup.closed) {
209
253
  popup.location.href = url;
@@ -220,50 +264,37 @@ export function useGoogleDesktopAuth(options: DesktopAuthOptions = {}) {
220
264
  await onSuccess?.(result);
221
265
  };
222
266
 
223
- if (startOptions.addAccount) {
224
- popup = window.open("about:blank", "_blank");
225
- void (async () => {
226
- try {
227
- const { url } = await fetchJson<{ url: string }>(
228
- agentNativePath(
229
- `/_agent-native/google/add-account/auth-url?${params.toString()}`,
230
- ),
231
- { credentials: "include" },
232
- );
233
- if (!openAuthUrl(url)) {
234
- reportError({
235
- code: "popup_blocked",
236
- message:
237
- "Calendar could not open Google sign-in. Allow popups and try again.",
238
- });
239
- }
240
- } catch (err) {
241
- popup?.close();
267
+ popup = window.open("about:blank", "_blank");
268
+ if (!popup) {
269
+ reportError({
270
+ code: "popup_blocked",
271
+ message:
272
+ "Calendar could not open Google sign-in. Allow popups and try again.",
273
+ });
274
+ return true;
275
+ }
276
+
277
+ void (async () => {
278
+ try {
279
+ const path = startOptions.addAccount
280
+ ? "/_agent-native/google/add-account/auth-url"
281
+ : "/_agent-native/google/auth-url";
282
+ const { url } = await fetchJson<{ url: string }>(
283
+ agentNativePath(`${path}?${params.toString()}`),
284
+ { credentials: "include" },
285
+ );
286
+ if (!openAuthUrl(url)) {
242
287
  reportError({
243
- code: "desktop_auth_start_failed",
288
+ code: "popup_blocked",
244
289
  message:
245
- err instanceof Error
246
- ? err.message
247
- : "Could not start Google sign-in.",
290
+ "Calendar could not open Google sign-in. Allow popups and try again.",
248
291
  });
249
292
  }
250
- })();
251
- } else {
252
- params.set("redirect", "1");
253
- const opened = openAuthUrl(
254
- `${window.location.origin}${agentNativePath(
255
- "/_agent-native/google/auth-url",
256
- )}?${params.toString()}`,
257
- );
258
- if (!opened) {
259
- reportError({
260
- code: "popup_blocked",
261
- message:
262
- "Calendar could not open Google sign-in. Allow popups and try again.",
263
- });
264
- return true;
293
+ } catch (err) {
294
+ popup?.close();
295
+ reportError(authStartIssue(err));
265
296
  }
266
- }
297
+ })();
267
298
 
268
299
  pollRef.current = setInterval(async () => {
269
300
  try {
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-30
4
+ ---
5
+
6
+ Google Calendar connection errors now stay in the app instead of opening a blank sign-in window.
@@ -43,6 +43,28 @@ const GOOGLE_IDENTITY_SCOPES = [
43
43
  "https://www.googleapis.com/auth/userinfo.profile",
44
44
  ];
45
45
 
46
+ type CalendarOAuthStateOptions = {
47
+ redirectUri: string;
48
+ owner?: string;
49
+ orgId?: string;
50
+ desktop?: boolean;
51
+ addAccount?: boolean;
52
+ app?: string;
53
+ returnUrl?: string;
54
+ flowId?: string;
55
+ };
56
+
57
+ function encodeCalendarOAuthState(options: CalendarOAuthStateOptions): string {
58
+ return encodeOAuthState(options as any);
59
+ }
60
+
61
+ function getCalendarOAuthStateOrgId(
62
+ state: ReturnType<typeof decodeOAuthState>,
63
+ ): string | undefined {
64
+ return (state as ReturnType<typeof decodeOAuthState> & { orgId?: string })
65
+ .orgId;
66
+ }
67
+
46
68
  async function resolveCalendarOAuthCredentials(event: H3Event) {
47
69
  const session = await getSession(event).catch(() => null);
48
70
  const { clientId, clientSecret } = await runWithRequestContext(
@@ -182,6 +204,28 @@ function googleOAuthErrorResponse(
182
204
  return oauthErrorPage(payload.message);
183
205
  }
184
206
 
207
+ function missingCredentialsResponse(
208
+ event: H3Event,
209
+ message: string,
210
+ opts: { desktop?: boolean; flowId?: string; redirect?: boolean } = {},
211
+ ) {
212
+ if (opts.desktop && opts.flowId) {
213
+ setDesktopExchangeError(opts.flowId, {
214
+ message,
215
+ code: "missing_credentials",
216
+ });
217
+ return oauthDesktopExchangePage("Returning to Calendar...");
218
+ }
219
+ if (opts.redirect) {
220
+ return oauthErrorPage(message);
221
+ }
222
+ setResponseStatus(event, 422);
223
+ return {
224
+ error: "missing_credentials",
225
+ message,
226
+ };
227
+ }
228
+
185
229
  export const getGoogleAuthUrl = defineEventHandler(async (event: H3Event) => {
186
230
  try {
187
231
  const q = getQuery(event);
@@ -195,6 +239,7 @@ export const getGoogleAuthUrl = defineEventHandler(async (event: H3Event) => {
195
239
  }
196
240
  const session = await getSession(event);
197
241
  const owner = session?.email;
242
+ const orgId = session?.orgId;
198
243
  const desktop =
199
244
  isElectron(event) || q.desktop === "1" || q.desktop === "true";
200
245
  const flowId = desktop ? (q.flow_id as string) || undefined : undefined;
@@ -204,13 +249,13 @@ export const getGoogleAuthUrl = defineEventHandler(async (event: H3Event) => {
204
249
  : resolveGoogleSignInCredentials();
205
250
 
206
251
  if (!credentials) {
207
- setResponseStatus(event, 422);
208
- return {
209
- error: "missing_credentials",
210
- message: calendarConnect
252
+ return missingCredentialsResponse(
253
+ event,
254
+ calendarConnect
211
255
  ? "Google Calendar OAuth credentials are not configured. Save GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in settings."
212
256
  : "Google sign-in credentials are not configured. Set GOOGLE_SIGN_IN_CLIENT_ID and GOOGLE_SIGN_IN_CLIENT_SECRET.",
213
- };
257
+ { desktop, flowId, redirect: q.redirect === "1" },
258
+ );
214
259
  }
215
260
 
216
261
  if (calendarConnect && !owner) {
@@ -226,9 +271,10 @@ export const getGoogleAuthUrl = defineEventHandler(async (event: H3Event) => {
226
271
  const returnUrl = requestedReturn !== "/" ? requestedReturn : undefined;
227
272
  // Use the named-arg overload — the positional form previously passed
228
273
  // `flowId` in the `returnUrl` slot, breaking desktop completion.
229
- const state = encodeOAuthState({
274
+ const state = encodeCalendarOAuthState({
230
275
  redirectUri,
231
276
  owner,
277
+ orgId,
232
278
  desktop,
233
279
  addAccount: calendarConnect,
234
280
  app: OAUTH_STATE_APP_ID,
@@ -237,7 +283,7 @@ export const getGoogleAuthUrl = defineEventHandler(async (event: H3Event) => {
237
283
  });
238
284
 
239
285
  const url = calendarConnect
240
- ? await getAuthUrl(undefined, redirectUri, state, owner)
286
+ ? await getAuthUrl(undefined, redirectUri, state, owner, orgId)
241
287
  : `${GOOGLE_AUTH_URL}?${new URLSearchParams({
242
288
  client_id: credentials.clientId,
243
289
  redirect_uri: redirectUri,
@@ -293,6 +339,7 @@ export const handleGoogleCallback = defineEventHandler(
293
339
  }
294
340
 
295
341
  const { redirectUri, owner: stateOwner, addAccount, returnUrl } = state;
342
+ const stateOrgId = getCalendarOAuthStateOrgId(state);
296
343
 
297
344
  // 1. Resolve owner (needs session context, before exchangeCode)
298
345
  const { owner, hasProductionSession } = await resolveOAuthOwner(
@@ -330,7 +377,13 @@ export const handleGoogleCallback = defineEventHandler(
330
377
  }
331
378
 
332
379
  // 2. Exchange code with Google (template-specific Calendar connect)
333
- const email = await exchangeCode(code, undefined, redirectUri, owner);
380
+ const email = await exchangeCode(
381
+ code,
382
+ undefined,
383
+ redirectUri,
384
+ owner,
385
+ stateOrgId,
386
+ );
334
387
 
335
388
  // 3. Create session token (after we have the email)
336
389
  // Skip for add-account flows — adding a second account must not switch
@@ -372,16 +425,18 @@ export const getGoogleAddAccountUrl = defineEventHandler(
372
425
  setResponseStatus(event, 401);
373
426
  return { error: "Must be logged in to add an account" };
374
427
  }
428
+ const q = getQuery(event);
429
+ const desktop =
430
+ isElectron(event) || q.desktop === "1" || q.desktop === "true";
431
+ const flowId = desktop ? (q.flow_id as string) || undefined : undefined;
375
432
  if (!(await resolveCalendarOAuthCredentials(event))) {
376
- setResponseStatus(event, 422);
377
- return {
378
- error: "missing_credentials",
379
- message:
380
- "Google OAuth credentials are not configured. Save GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in settings.",
381
- };
433
+ return missingCredentialsResponse(
434
+ event,
435
+ "Google OAuth credentials are not configured. Save GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in settings.",
436
+ { desktop, flowId, redirect: q.redirect === "1" },
437
+ );
382
438
  }
383
439
  try {
384
- const q = getQuery(event);
385
440
  const redirectUri = resolveOAuthRedirectUri(event);
386
441
  if (!redirectUri) {
387
442
  setResponseStatus(event, 400);
@@ -390,12 +445,10 @@ export const getGoogleAddAccountUrl = defineEventHandler(
390
445
  message: "redirect_uri must stay on this app's _agent-native routes.",
391
446
  };
392
447
  }
393
- const desktop =
394
- isElectron(event) || q.desktop === "1" || q.desktop === "true";
395
- const flowId = desktop ? (q.flow_id as string) || undefined : undefined;
396
- const state = encodeOAuthState({
448
+ const state = encodeCalendarOAuthState({
397
449
  redirectUri,
398
450
  owner: session.email,
451
+ orgId: session.orgId,
399
452
  desktop,
400
453
  addAccount: true,
401
454
  app: OAUTH_STATE_APP_ID,
@@ -406,6 +459,7 @@ export const getGoogleAddAccountUrl = defineEventHandler(
406
459
  redirectUri,
407
460
  state,
408
461
  session.email,
462
+ session.orgId,
409
463
  );
410
464
  if (q.redirect === "1") {
411
465
  return oauthRedirectResponse(url);
@@ -449,6 +503,7 @@ export const handleGoogleAddAccountCallback = defineEventHandler(
449
503
  }
450
504
 
451
505
  const { redirectUri, owner: stateOwner } = state;
506
+ const stateOrgId = getCalendarOAuthStateOrgId(state);
452
507
 
453
508
  const ownerEmail = session?.email || stateOwner;
454
509
  if (!ownerEmail) {
@@ -466,6 +521,7 @@ export const handleGoogleAddAccountCallback = defineEventHandler(
466
521
  undefined,
467
522
  redirectUri,
468
523
  ownerEmail,
524
+ session?.orgId ?? stateOrgId,
469
525
  );
470
526
 
471
527
  return oauthCallbackResponse(event, addedEmail, {
@@ -486,7 +542,7 @@ export const handleGoogleAddAccountCallback = defineEventHandler(
486
542
  export const getGoogleStatus = defineEventHandler(async (event: H3Event) => {
487
543
  try {
488
544
  const session = await getSession(event);
489
- return await getAuthStatus(session?.email);
545
+ return await getAuthStatus(session?.email, session?.orgId);
490
546
  } catch (error: any) {
491
547
  setResponseStatus(event, 500);
492
548
  return { error: error.message };
@@ -506,7 +562,7 @@ export const disconnectGoogle = defineEventHandler(async (event: H3Event) => {
506
562
  setResponseStatus(event, 400);
507
563
  return { error: "email is required" };
508
564
  }
509
- const owned = await getAuthStatus(session.email);
565
+ const owned = await getAuthStatus(session.email, session.orgId);
510
566
  const isOwned = owned.accounts.some((a) => a.email === targetEmail);
511
567
  if (!isOwned) {
512
568
  setResponseStatus(event, 403);