@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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +12 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/AgentPanel.tsx +33 -1
- package/corpus/core/src/server/core-routes-plugin.ts +4 -5
- package/corpus/core/src/server/create-server.ts +3 -5
- package/corpus/core/src/server/google-oauth.ts +4 -0
- package/corpus/templates/calendar/actions/create-event.ts +9 -2
- package/corpus/templates/calendar/actions/event-action-helpers.ts +8 -2
- package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +11 -0
- package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +17 -0
- package/corpus/templates/calendar/app/hooks/use-google-auth.ts +70 -39
- package/corpus/templates/calendar/changelog/2026-06-30-google-calendar-connection-errors-now-stay-in-the-app-instea.md +6 -0
- package/corpus/templates/calendar/server/handlers/google-auth.ts +77 -21
- package/corpus/templates/calendar/server/lib/google-calendar.ts +19 -8
- package/corpus/templates/design/actions/apply-design-token-edit.ts +13 -6
- package/corpus/templates/design/actions/import-design-tokens.ts +17 -13
- package/corpus/templates/design/actions/preview-design-token-edit.ts +20 -3
- package/corpus/templates/design/actions/run-design-audit.ts +9 -2
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +35 -14
- package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +20 -3
- package/corpus/templates/design/app/pages/DesignEditor.tsx +237 -51
- package/corpus/templates/design/app/pages/VisualEdit.tsx +1 -1
- package/corpus/templates/design/changelog/2026-06-30-design-editor-undo-drag-drop-style-editing-and-visual-edit-s.md +6 -0
- package/corpus/templates/design/shared/resolve-tweaks.ts +29 -6
- package/dist/client/AgentPanel.d.ts +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +18 -3
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +1 -2
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/create-server.d.ts.map +1 -1
- package/dist/server/create-server.js +1 -2
- package/dist/server/create-server.js.map +1 -1
- package/dist/server/google-oauth.d.ts +2 -0
- package/dist/server/google-oauth.d.ts.map +1 -1
- package/dist/server/google-oauth.js +3 -0
- package/dist/server/google-oauth.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -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
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.81.
|
|
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
|
-
|
|
2321
|
-
(
|
|
2322
|
-
|
|
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
|
-
|
|
226
|
-
|
|
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 {
|
|
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(
|
|
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 {
|
|
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(
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
|
|
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: "
|
|
288
|
+
code: "popup_blocked",
|
|
244
289
|
message:
|
|
245
|
-
|
|
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
|
-
|
|
252
|
-
|
|
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 {
|
|
@@ -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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
|
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);
|