@agent-native/core 0.81.1 → 0.81.3
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 +25 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/frame.ts +55 -0
- package/corpus/core/src/mcp/build-server.ts +30 -4
- package/corpus/core/src/mcp/embed-app.ts +33 -18
- 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/DesignCanvas.tsx +8 -1
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +11 -2
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +89 -16
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +21 -14
- 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 +794 -262
- 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/changelog/2026-06-30-design-studio-canvas-layers-can-be-copied-pasted-and-reparen.md +6 -0
- package/corpus/templates/design/e2e/global-setup.ts +3 -1
- package/corpus/templates/design/e2e/helpers.ts +4 -1
- package/corpus/templates/design/playwright.config.ts +4 -1
- package/corpus/templates/design/shared/resolve-tweaks.ts +29 -6
- package/corpus/templates/plan/actions/consume-plan-feedback.ts +1 -9
- package/corpus/templates/plan/actions/convert-visual-plan-to-prototype.ts +1 -1
- package/corpus/templates/plan/actions/create-plan-design.ts +1 -1
- package/corpus/templates/plan/actions/create-prototype-plan.ts +1 -1
- package/corpus/templates/plan/actions/create-ui-plan.ts +1 -1
- package/corpus/templates/plan/actions/create-visual-plan.ts +36 -19
- package/corpus/templates/plan/actions/create-visual-questions.ts +1 -1
- package/corpus/templates/plan/actions/create-visual-recap.ts +1 -1
- package/corpus/templates/plan/actions/delete-plan-comment.ts +1 -9
- package/corpus/templates/plan/actions/delete-visual-plan.ts +1 -9
- package/corpus/templates/plan/actions/get-plan-feedback.ts +1 -9
- package/corpus/templates/plan/actions/get-visual-plan.ts +1 -9
- package/corpus/templates/plan/actions/import-visual-plan-source.ts +1 -1
- package/corpus/templates/plan/actions/list-visual-plans.ts +1 -9
- package/corpus/templates/plan/actions/reply-to-plan-comment.ts +1 -9
- package/corpus/templates/plan/actions/resolve-plan-comment.ts +1 -9
- package/corpus/templates/plan/actions/update-visual-plan.ts +1 -1
- package/corpus/templates/plan/actions/visual-answer.ts +1 -1
- package/corpus/templates/plan/app/components/layout/Layout.tsx +15 -0
- package/corpus/templates/plan/app/entry.client.tsx +10 -0
- package/corpus/templates/plan/app/global.css +28 -0
- package/corpus/templates/plan/shared/plan-content.ts +10 -6
- package/dist/client/frame.d.ts.map +1 -1
- package/dist/client/frame.js +59 -0
- package/dist/client/frame.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 +1 -1
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +31 -4
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/embed-app.d.ts.map +1 -1
- package/dist/mcp/embed-app.js +33 -18
- package/dist/mcp/embed-app.js.map +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/resources/handlers.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- 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,30 @@
|
|
|
1
1
|
# @agent-native/core
|
|
2
2
|
|
|
3
|
+
## 0.81.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 3807702: Fix MCP App embed rendering in ChatGPT/Codex hosts:
|
|
8
|
+
- Stop the `openai:set_globals` storm that left embeds stuck blanking by
|
|
9
|
+
guarding the bridge sync on a signature that ignores host `maxHeight`, and
|
|
10
|
+
by not re-blanking once the app frame has launched.
|
|
11
|
+
- Size embeds to their content: the embedded app reports its real
|
|
12
|
+
`scrollHeight` via `agentNative.contentHeight` and the shell sizes the iframe
|
|
13
|
+
to that (plus chrome) instead of the host max, so plans no longer render far
|
|
14
|
+
too tall or too short.
|
|
15
|
+
- Suppress empty embeds: when a tool whose descriptor declares an embed widget
|
|
16
|
+
produces no embeddable content, the result is marked `isError` so the host
|
|
17
|
+
shows the text result without an empty widget box. This also keeps read-only
|
|
18
|
+
and comment-mutation tools from rendering an embed for results that produce
|
|
19
|
+
no plan surface.
|
|
20
|
+
- Bump the embed shell resource version so hosts refetch the updated shell.
|
|
21
|
+
|
|
22
|
+
## 0.81.2
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- ea1cc47: Keep OAuth state and environment status checks aligned with scoped workspace credentials.
|
|
27
|
+
|
|
3
28
|
## 0.81.1
|
|
4
29
|
|
|
5
30
|
### Patch 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.3",
|
|
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": {
|
|
@@ -83,6 +83,60 @@ export function isTrustedFrameMessage(event: MessageEvent): boolean {
|
|
|
83
83
|
return event.source === window.parent || event.source === window;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Content-height reporting (for MCP App embeds)
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
//
|
|
90
|
+
// The embed shell renders this app in a cross-origin iframe and can't measure
|
|
91
|
+
// its content, so we post the real content height for the shell to size to.
|
|
92
|
+
// Only shrinks when the document flows to its natural height — embed layouts
|
|
93
|
+
// must not force `100vh`/`h-screen` on the root, or scrollHeight just equals
|
|
94
|
+
// the iframe height.
|
|
95
|
+
let _contentHeightReportingStarted = false;
|
|
96
|
+
let _lastReportedContentHeight = 0;
|
|
97
|
+
|
|
98
|
+
function measureContentHeight(): number {
|
|
99
|
+
if (typeof document === "undefined") return 0;
|
|
100
|
+
const doc = document.documentElement;
|
|
101
|
+
const body = document.body;
|
|
102
|
+
return Math.ceil(
|
|
103
|
+
Math.max(doc ? doc.scrollHeight : 0, body ? body.scrollHeight : 0),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function reportContentHeight(): void {
|
|
108
|
+
if (typeof window === "undefined" || window.parent === window) return;
|
|
109
|
+
const height = measureContentHeight();
|
|
110
|
+
if (!height || Math.abs(height - _lastReportedContentHeight) < 2) return;
|
|
111
|
+
_lastReportedContentHeight = height;
|
|
112
|
+
sendToFrame("agentNative.contentHeight", { height });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function startContentHeightReporting(): void {
|
|
116
|
+
if (_contentHeightReportingStarted || typeof window === "undefined") return;
|
|
117
|
+
if (window.parent === window) return;
|
|
118
|
+
_contentHeightReportingStarted = true;
|
|
119
|
+
let scheduled = false;
|
|
120
|
+
const schedule = () => {
|
|
121
|
+
if (scheduled) return;
|
|
122
|
+
scheduled = true;
|
|
123
|
+
window.requestAnimationFrame(() => {
|
|
124
|
+
scheduled = false;
|
|
125
|
+
reportContentHeight();
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
schedule();
|
|
129
|
+
for (const delay of [100, 300, 800, 1500]) window.setTimeout(schedule, delay);
|
|
130
|
+
try {
|
|
131
|
+
const observer = new ResizeObserver(schedule);
|
|
132
|
+
if (document.documentElement) observer.observe(document.documentElement);
|
|
133
|
+
if (document.body) observer.observe(document.body);
|
|
134
|
+
} catch {
|
|
135
|
+
window.addEventListener("resize", schedule, { passive: true });
|
|
136
|
+
}
|
|
137
|
+
window.addEventListener("load", schedule, { passive: true });
|
|
138
|
+
}
|
|
139
|
+
|
|
86
140
|
// Listen for frame origin message and cache it.
|
|
87
141
|
// Only accept from the direct parent frame, and only set once.
|
|
88
142
|
if (typeof window !== "undefined") {
|
|
@@ -102,6 +156,7 @@ if (typeof window !== "undefined") {
|
|
|
102
156
|
{ type: "agentNative.embeddedAppReady" },
|
|
103
157
|
getFramePostMessageTargetOrigin() ?? window.location.origin,
|
|
104
158
|
);
|
|
159
|
+
startContentHeightReporting();
|
|
105
160
|
}
|
|
106
161
|
});
|
|
107
162
|
}
|
|
@@ -448,6 +448,18 @@ function purgeEmbedStartUrls(value: unknown, depth = 0): unknown {
|
|
|
448
448
|
return value;
|
|
449
449
|
}
|
|
450
450
|
|
|
451
|
+
// True when a tool result carries SOME content. An errored/no-plan result comes
|
|
452
|
+
// back empty (`{}` / null) and would render an empty embed box. We gate on "has
|
|
453
|
+
// content", not "has a URL" — valid embeds often carry data but no URL (the
|
|
454
|
+
// shell mints the embed-start itself).
|
|
455
|
+
function mcpResultHasContent(result: unknown): boolean {
|
|
456
|
+
if (result == null) return false;
|
|
457
|
+
if (typeof result === "string") return result.trim().length > 0;
|
|
458
|
+
if (Array.isArray(result)) return result.length > 0;
|
|
459
|
+
if (typeof result === "object") return Object.keys(result).length > 0;
|
|
460
|
+
return Boolean(result);
|
|
461
|
+
}
|
|
462
|
+
|
|
451
463
|
function mcpAppEmbedOpenLinkMeta(
|
|
452
464
|
result: unknown,
|
|
453
465
|
resource: ResolvedMcpAppResource,
|
|
@@ -750,7 +762,7 @@ function safeUiSegment(value: string | undefined, fallback: string): string {
|
|
|
750
762
|
|
|
751
763
|
// ChatGPT and Claude cache MCP App resource HTML by `ui://` URI. Bump this
|
|
752
764
|
// when the shared shell changes in a way that must invalidate host caches.
|
|
753
|
-
const MCP_APP_RESOURCE_SHELL_VERSION = "shell-
|
|
765
|
+
const MCP_APP_RESOURCE_SHELL_VERSION = "shell-v64";
|
|
754
766
|
|
|
755
767
|
function legacyDefaultMcpAppUri(config: MCPConfig, actionName: string): string {
|
|
756
768
|
const app = safeUiSegment(config.appId ?? config.name, "agent-native");
|
|
@@ -1546,12 +1558,24 @@ export async function createMCPServerForRequest(
|
|
|
1546
1558
|
// branch below degrades to the plain deep-link artifacts the tool would
|
|
1547
1559
|
// otherwise return — no `openai/outputTemplate`, no minted embed-start,
|
|
1548
1560
|
// no embed structuredContent — so the host shows a link, not an iframe.
|
|
1549
|
-
const
|
|
1561
|
+
const mcpAppResourceCandidate = requestMeta?.inlineMcpApps
|
|
1550
1562
|
? await resolveMcpAppResourceSafely(config, name, entry, requestMeta)
|
|
1551
1563
|
: null;
|
|
1552
|
-
const rawResultForClient =
|
|
1564
|
+
const rawResultForClient = mcpAppResourceCandidate
|
|
1553
1565
|
? await withServerMintedMcpAppEmbedStart(rawResult, requestMeta)
|
|
1554
1566
|
: rawResult;
|
|
1567
|
+
// Only attach the embed widget for a non-error result that has content.
|
|
1568
|
+
const embedHasContent = mcpResultHasContent(rawResultForClient);
|
|
1569
|
+
const mcpAppResource =
|
|
1570
|
+
mcpAppResourceCandidate && !mcpResultIsError && embedHasContent
|
|
1571
|
+
? mcpAppResourceCandidate
|
|
1572
|
+
: null;
|
|
1573
|
+
// `openai/outputTemplate` is declared at the tool level, so the host
|
|
1574
|
+
// renders a widget for every call regardless of result _meta. The only
|
|
1575
|
+
// per-result signal it honors is `isError` (shows error text, no widget),
|
|
1576
|
+
// so treat an embed tool that produced nothing as an error.
|
|
1577
|
+
const embedProducedNothing =
|
|
1578
|
+
!!mcpAppResourceCandidate && !mcpResultIsError && !embedHasContent;
|
|
1555
1579
|
const { block, _meta } = buildLinkArtifacts(
|
|
1556
1580
|
entry,
|
|
1557
1581
|
(args as Record<string, any>) ?? {},
|
|
@@ -1590,7 +1614,9 @@ export async function createMCPServerForRequest(
|
|
|
1590
1614
|
if (block) content.push(block);
|
|
1591
1615
|
return {
|
|
1592
1616
|
content,
|
|
1593
|
-
...(mcpResultIsError
|
|
1617
|
+
...(mcpResultIsError || embedProducedNothing
|
|
1618
|
+
? { isError: true }
|
|
1619
|
+
: {}),
|
|
1594
1620
|
...(structuredContent ? { structuredContent } : {}),
|
|
1595
1621
|
...(Object.keys(responseMeta).length > 0
|
|
1596
1622
|
? { _meta: responseMeta }
|
|
@@ -138,6 +138,7 @@ export function embedApp(
|
|
|
138
138
|
let appFrameLoadTimer = null;
|
|
139
139
|
let lastFrameSrc = "";
|
|
140
140
|
let embedSessionRefreshAttempts = 0;
|
|
141
|
+
let reportedContentHeight = 0;
|
|
141
142
|
|
|
142
143
|
function esc(value) {
|
|
143
144
|
return String(value ?? "")
|
|
@@ -174,10 +175,18 @@ export function embedApp(
|
|
|
174
175
|
function visibleIntrinsicHeight() {
|
|
175
176
|
const context = hostState().context || {};
|
|
176
177
|
const hostMaxHeight = contextMaxHeight(context);
|
|
178
|
+
// Size to the app-reported content height (the shell can't measure the
|
|
179
|
+
// cross-origin frame). Don't clamp to the host maxHeight — on Codex it's a
|
|
180
|
+
// small inline hint (~360) that clips the plan; applyIntrinsicHeight still
|
|
181
|
+
// caps at defaultIntrinsicHeight.
|
|
182
|
+
if (reportedContentHeight > 0) {
|
|
183
|
+
return Math.floor(reportedContentHeight + chromeHeight);
|
|
184
|
+
}
|
|
177
185
|
if (hostMaxHeight) return Math.floor(hostMaxHeight);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
186
|
+
// No host maxHeight and no content height yet: use the CONFIGURED height,
|
|
187
|
+
// never window.innerHeight. Echoing the iframe's own size pins us to the
|
|
188
|
+
// current frame height (circular) and locks the embed at the cap forever.
|
|
189
|
+
return Math.floor(defaultIntrinsicHeight);
|
|
181
190
|
}
|
|
182
191
|
|
|
183
192
|
function applyIntrinsicHeight(nextHeight) {
|
|
@@ -1356,7 +1365,8 @@ export function embedApp(
|
|
|
1356
1365
|
}
|
|
1357
1366
|
|
|
1358
1367
|
function notifyHostHeight() {
|
|
1359
|
-
const
|
|
1368
|
+
const intrinsic = visibleIntrinsicHeight();
|
|
1369
|
+
const height = applyIntrinsicHeight(intrinsic);
|
|
1360
1370
|
if (!openAiBridge || typeof openAiBridge.notifyIntrinsicHeight !== "function") {
|
|
1361
1371
|
if (app && typeof app.sendSizeChanged === "function") {
|
|
1362
1372
|
try {
|
|
@@ -1487,6 +1497,14 @@ export function embedApp(
|
|
|
1487
1497
|
notifyOuterMcpAppReady();
|
|
1488
1498
|
return;
|
|
1489
1499
|
}
|
|
1500
|
+
if (event.data.type === "agentNative.contentHeight") {
|
|
1501
|
+
const next = finiteNumber(data && data.height);
|
|
1502
|
+
if (next && Math.abs(next - reportedContentHeight) >= 1) {
|
|
1503
|
+
reportedContentHeight = next;
|
|
1504
|
+
notifyHostHeightSoon();
|
|
1505
|
+
}
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1490
1508
|
if (event.data.type === "agentNative.embedSessionExpired") {
|
|
1491
1509
|
refreshExpiredEmbedSession();
|
|
1492
1510
|
return;
|
|
@@ -1538,12 +1556,9 @@ export function embedApp(
|
|
|
1538
1556
|
const selfNavigate = shouldSelfNavigateToApp();
|
|
1539
1557
|
if (startedFor === launchUrl) return;
|
|
1540
1558
|
startedFor = launchUrl;
|
|
1541
|
-
// Only show
|
|
1542
|
-
//
|
|
1543
|
-
//
|
|
1544
|
-
// the stage on every redundant launch, so the app rendered and was
|
|
1545
|
-
// instantly blanked back to "Loading app" forever. renderFrame()/
|
|
1546
|
-
// transplant replace the stage themselves when they actually (re)mount.
|
|
1559
|
+
// Only show "Loading app" when no frame is mounted. set_globals
|
|
1560
|
+
// re-launches constantly, so calling it every time would wipe the
|
|
1561
|
+
// just-mounted iframe; renderFrame/transplant own the stage on (re)mount.
|
|
1547
1562
|
if (!appFrame) {
|
|
1548
1563
|
setMessage("Loading app");
|
|
1549
1564
|
}
|
|
@@ -1671,14 +1686,11 @@ export function embedApp(
|
|
|
1671
1686
|
toolResultData = objectValue(data);
|
|
1672
1687
|
openUrl = openLinkFrom(params, data);
|
|
1673
1688
|
openStartUrl = embedStartUrlFrom(params, data);
|
|
1674
|
-
//
|
|
1675
|
-
//
|
|
1676
|
-
//
|
|
1677
|
-
//
|
|
1678
|
-
//
|
|
1679
|
-
// the host round-trips + (re)launch when something we care about changed.
|
|
1680
|
-
// Deliberately EXCLUDE bridge.maxHeight: the host mutates it on every
|
|
1681
|
-
// set_globals as it echoes our height back, which would defeat the guard.
|
|
1689
|
+
// set_globals fires constantly, and this sync calls notifyHostHeight/
|
|
1690
|
+
// sendHostContext which the host echoes back as another set_globals — an
|
|
1691
|
+
// infinite storm. Only do the host round-trips + (re)launch when something
|
|
1692
|
+
// we care about changed. Exclude maxHeight: the host mutates it every
|
|
1693
|
+
// event, which would defeat the guard.
|
|
1682
1694
|
let signature;
|
|
1683
1695
|
try {
|
|
1684
1696
|
signature = JSON.stringify([
|
|
@@ -1704,6 +1716,9 @@ export function embedApp(
|
|
|
1704
1716
|
if (openUrl || openStartUrl) {
|
|
1705
1717
|
void launchEmbed();
|
|
1706
1718
|
} else if (!appFrame) {
|
|
1719
|
+
// No open URL yet — a content-less result no longer reaches the host as
|
|
1720
|
+
// a widget (build-server marks it isError), so this is the transient
|
|
1721
|
+
// "waiting for the embed to resolve" state, not an empty box.
|
|
1707
1722
|
setMessage("Waiting for app result");
|
|
1708
1723
|
}
|
|
1709
1724
|
return true;
|
|
@@ -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 {
|