@agent-native/dispatch 0.27.6 → 0.27.8

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 (28) hide show
  1. package/dist/actions/create_workspace_app_embed_session.d.ts +6 -0
  2. package/dist/actions/create_workspace_app_embed_session.d.ts.map +1 -1
  3. package/dist/actions/create_workspace_app_embed_session.js +49 -1
  4. package/dist/actions/create_workspace_app_embed_session.js.map +1 -1
  5. package/dist/actions/delete-destination.d.ts +12 -12
  6. package/dist/actions/delete-workspace-resource.d.ts +13 -13
  7. package/dist/actions/update-workspace-resource.d.ts +13 -13
  8. package/dist/components/workspace-app-host.d.ts.map +1 -1
  9. package/dist/components/workspace-app-host.js +15 -53
  10. package/dist/components/workspace-app-host.js.map +1 -1
  11. package/dist/server/lib/mcp-gateway.d.ts.map +1 -1
  12. package/dist/server/lib/mcp-gateway.js +2 -1
  13. package/dist/server/lib/mcp-gateway.js.map +1 -1
  14. package/dist/server/lib/workspace-app-action-auth.d.ts.map +1 -1
  15. package/dist/server/lib/workspace-app-action-auth.js +15 -0
  16. package/dist/server/lib/workspace-app-action-auth.js.map +1 -1
  17. package/dist/shared/feature-flags.d.ts +1 -0
  18. package/dist/shared/feature-flags.d.ts.map +1 -1
  19. package/dist/shared/feature-flags.js +5 -0
  20. package/dist/shared/feature-flags.js.map +1 -1
  21. package/package.json +2 -2
  22. package/src/actions/create_workspace_app_embed_session.ts +60 -1
  23. package/src/components/workspace-app-host.spec.tsx +22 -0
  24. package/src/components/workspace-app-host.tsx +14 -80
  25. package/src/server/lib/mcp-gateway.spec.ts +5 -0
  26. package/src/server/lib/mcp-gateway.ts +7 -1
  27. package/src/server/lib/workspace-app-action-auth.ts +17 -0
  28. package/src/shared/feature-flags.ts +7 -0
@@ -11,12 +11,7 @@ import {
11
11
  } from "@agent-native/core/client/hooks";
12
12
  import { useT } from "@agent-native/core/client/i18n";
13
13
  import { withBuilderUtmTrackingParams } from "@agent-native/core/shared/builder-link-tracking";
14
- import {
15
- IconArrowLeft,
16
- IconArrowUpRight,
17
- IconClockHour4,
18
- IconLock,
19
- } from "@tabler/icons-react";
14
+ import { IconArrowLeft, IconClockHour4 } from "@tabler/icons-react";
20
15
  import { useTheme } from "next-themes";
21
16
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
22
17
  import { Link } from "react-router";
@@ -57,31 +52,6 @@ interface GrantedWorkspaceAppsResult {
57
52
  }
58
53
 
59
54
  type WorkspaceAppTheme = "light" | "dark";
60
- type WorkspaceAppAuthState = "unknown" | "authenticated" | "unauthenticated";
61
-
62
- function resolveWorkspaceAppAuthState(
63
- rawUrl: string | null,
64
- ): WorkspaceAppAuthState {
65
- if (!rawUrl) return "unknown";
66
- try {
67
- const lastSegment = new URL(rawUrl, window.location.origin).pathname
68
- .split("/")
69
- .filter(Boolean)
70
- .at(-1)
71
- ?.toLowerCase();
72
- if (
73
- lastSegment === "sign-in" ||
74
- lastSegment === "login" ||
75
- lastSegment === "signup"
76
- ) {
77
- return "unauthenticated";
78
- }
79
- return "authenticated";
80
- } catch {
81
- return "unknown";
82
- }
83
- }
84
-
85
55
  function buildWorkspaceAppThemeUpdate(theme: WorkspaceAppTheme) {
86
56
  return {
87
57
  type: "agent-native-theme-update" as const,
@@ -131,7 +101,6 @@ export function WorkspaceAppFrame({
131
101
  const [embedError, setEmbedError] = useState<Error | null>(null);
132
102
  const [isDirectFallback, setIsDirectFallback] = useState(false);
133
103
  const [embedAttempt, setEmbedAttempt] = useState(0);
134
- const [authState, setAuthState] = useState<WorkspaceAppAuthState>("unknown");
135
104
  const embedFrameRef = useRef<HTMLIFrameElement>(null);
136
105
  const postThemeToFrame = useCallback(() => {
137
106
  embedFrameRef.current?.contentWindow?.postMessage(
@@ -142,17 +111,7 @@ export function WorkspaceAppFrame({
142
111
  const handleFrameLoad = useCallback(() => {
143
112
  postThemeToFrame();
144
113
  if (isDirectFallback) setEmbedError(null);
145
- const frame = embedFrameRef.current;
146
- let frameUrl = embedUrl;
147
- try {
148
- frameUrl = frame?.contentWindow?.location.href ?? frameUrl;
149
- // coercion-ok: Cross-origin frames cannot expose location; their auth state arrives by message.
150
- } catch {
151
- // Cross-origin app frames report their auth state through postMessage.
152
- }
153
- const nextAuthState = resolveWorkspaceAppAuthState(frameUrl);
154
- if (nextAuthState !== "unknown") setAuthState(nextAuthState);
155
- }, [embedUrl, isDirectFallback, postThemeToFrame]);
114
+ }, [isDirectFallback, postThemeToFrame]);
156
115
  const workspaceSsoEnabled = useFeatureFlag(DISPATCH_WORKSPACE_SSO_FLAG.key);
157
116
  const createEmbedSession = useActionMutation<
158
117
  EmbedSessionResult,
@@ -190,7 +149,6 @@ export function WorkspaceAppFrame({
190
149
  setEmbedUrl(null);
191
150
  setEmbedError(null);
192
151
  setIsDirectFallback(false);
193
- setAuthState("unknown");
194
152
  const createSession = workspaceSsoEnabled
195
153
  ? createWorkspaceSsoEmbedSession
196
154
  : createEmbedSession;
@@ -201,6 +159,17 @@ export function WorkspaceAppFrame({
201
159
  })
202
160
  .catch((cause: unknown) => {
203
161
  if (cancelled) return;
162
+ const error = cause instanceof Error ? cause : new Error(String(cause));
163
+ if (workspaceSsoEnabled) {
164
+ // An SSO-enabled pane must never fall back to the child app's
165
+ // unauthenticated shell. Keep the parent-owned retry surface in
166
+ // place so a transient exchange failure cannot expose another
167
+ // login form.
168
+ setIsDirectFallback(false);
169
+ setEmbedUrl(null);
170
+ setEmbedError(error);
171
+ return;
172
+ }
204
173
  setIsDirectFallback(true);
205
174
  setEmbedUrl(
206
175
  workspaceAppDirectHref(
@@ -208,9 +177,7 @@ export function WorkspaceAppFrame({
208
177
  embedPath ?? "/",
209
178
  ),
210
179
  );
211
- setEmbedError(
212
- cause instanceof Error ? cause : new Error(String(cause)),
213
- );
180
+ setEmbedError(error);
214
181
  });
215
182
  return () => {
216
183
  cancelled = true;
@@ -242,21 +209,6 @@ export function WorkspaceAppFrame({
242
209
  window.removeEventListener("message", handleEmbedSessionExpired);
243
210
  }, [embedUrl]);
244
211
 
245
- useEffect(() => {
246
- const handleAuthState = (event: MessageEvent) => {
247
- const frame = embedFrameRef.current;
248
- if (!frame || event.source !== frame.contentWindow) return;
249
- if (event.data?.type !== "agentNative.authState") return;
250
- const status = event.data.data?.status;
251
- if (status === "authenticated" || status === "unauthenticated") {
252
- setAuthState(status);
253
- }
254
- };
255
-
256
- window.addEventListener("message", handleAuthState);
257
- return () => window.removeEventListener("message", handleAuthState);
258
- }, []);
259
-
260
212
  useEffect(() => {
261
213
  postThemeToFrame();
262
214
  }, [embedUrl, postThemeToFrame]);
@@ -315,24 +267,6 @@ export function WorkspaceAppFrame({
315
267
  dynamicSuggestions={false}
316
268
  suggestions={[]}
317
269
  emptyStateText={`Ask about ${app.name}`}
318
- composerSlot={
319
- authState === "unauthenticated" ? (
320
- <div className="flex shrink-0 items-center px-3 pb-1">
321
- <button
322
- type="button"
323
- data-dispatch-app-sign-in
324
- aria-label={`Sign in to ${app.name} on the right`}
325
- title={`Sign in to ${app.name} on the right`}
326
- onClick={() => embedFrameRef.current?.focus()}
327
- className="inline-flex h-6 shrink-0 items-center gap-1 rounded-full border border-border/70 bg-background/60 px-2 text-[11px] font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
328
- >
329
- <IconLock size={12} stroke={1.8} />
330
- <span>Sign in on the right</span>
331
- <IconArrowUpRight size={12} stroke={1.8} />
332
- </button>
333
- </div>
334
- ) : null
335
- }
336
270
  >
337
271
  {appPane}
338
272
  </AgentSidebar>
@@ -17,6 +17,7 @@ const mocks = vi.hoisted(() => ({
17
17
  a2aSend: vi.fn(),
18
18
  a2aGetTask: vi.fn(),
19
19
  signA2AToken: vi.fn(),
20
+ canonicalA2AAudience: vi.fn((url: string) => url.replace(/\/+$/, "")),
20
21
  getOrgA2ASecret: vi.fn(),
21
22
  getOrgDomain: vi.fn(),
22
23
  isFeatureFlagEnabled: vi.fn(),
@@ -71,6 +72,7 @@ vi.mock("@agent-native/core/a2a", () => ({
71
72
  }
72
73
  },
73
74
  signA2AToken: mocks.signA2AToken,
75
+ canonicalA2AAudience: mocks.canonicalA2AAudience,
74
76
  }));
75
77
 
76
78
  vi.mock("@agent-native/core/org", () => ({
@@ -1255,6 +1257,7 @@ describe("createGrantedDispatchMcpEmbedSession", () => {
1255
1257
  "org-specific-secret",
1256
1258
  {
1257
1259
  expiresIn: "5m",
1260
+ audience: "http://localhost:8086",
1258
1261
  preferGlobalSecret: false,
1259
1262
  },
1260
1263
  );
@@ -1283,6 +1286,7 @@ describe("createGrantedDispatchMcpEmbedSession", () => {
1283
1286
  undefined,
1284
1287
  {
1285
1288
  expiresIn: "5m",
1289
+ audience: "http://localhost:8086",
1286
1290
  preferGlobalSecret: true,
1287
1291
  },
1288
1292
  );
@@ -1311,6 +1315,7 @@ describe("createGrantedDispatchMcpEmbedSession", () => {
1311
1315
  undefined,
1312
1316
  {
1313
1317
  expiresIn: "5m",
1318
+ audience: "http://localhost:8086",
1314
1319
  preferGlobalSecret: true,
1315
1320
  },
1316
1321
  );
@@ -1,4 +1,9 @@
1
- import { A2AClient, signA2AToken, type Task } from "@agent-native/core/a2a";
1
+ import {
2
+ A2AClient,
3
+ canonicalA2AAudience,
4
+ signA2AToken,
5
+ type Task,
6
+ } from "@agent-native/core/a2a";
2
7
  import { isFeatureFlagEnabled } from "@agent-native/core/feature-flags";
3
8
  import {
4
9
  buildMcpToolName,
@@ -1195,6 +1200,7 @@ async function createEmbedSessionForResolvedApp(input: {
1195
1200
  useOrgSigning ? orgSecret.trim() : undefined,
1196
1201
  {
1197
1202
  expiresIn: "5m",
1203
+ audience: canonicalA2AAudience(appBaseUrl(target.app)),
1198
1204
  // Prefer the synced org A2A secret when present because first-party
1199
1205
  // production apps do not have to share the same deployment env secret.
1200
1206
  // Fall back to the global A2A_SECRET for orgs that have not synced yet.
@@ -8,6 +8,7 @@ import type {
8
8
  ActionRouteAuthAdapter,
9
9
  ActionRouteResolvedCaller,
10
10
  } from "@agent-native/core/server";
11
+ import { getSession } from "@agent-native/core/server";
11
12
 
12
13
  export const WORKSPACE_APPS_ACTION_PATH =
13
14
  "/_agent-native/actions/list-workspace-apps";
@@ -57,6 +58,22 @@ export const workspaceAppActionRouteAuth: ActionRouteAuthAdapter = {
57
58
  throw new Error("Invalid workspace registry authorization");
58
59
  }
59
60
 
61
+ // Native clients use their normal persisted session bearer for the same
62
+ // read-only action. Let the framework resolve that credential before
63
+ // treating an otherwise opaque bearer as an A2A token.
64
+ const hasRequestUrl =
65
+ typeof event?.node?.req?.url === "string" ||
66
+ typeof event?.req?.url === "string" ||
67
+ typeof event?.url === "string";
68
+ const session = hasRequestUrl ? await getSession(event) : null;
69
+ if (session?.email) {
70
+ return {
71
+ owner: session.email,
72
+ anonymous: false,
73
+ ...(session.orgId ? { orgId: session.orgId } : {}),
74
+ };
75
+ }
76
+
60
77
  const token = authorization.slice("Bearer ".length).trim();
61
78
  if (!token) throw new Error("Invalid workspace registry authorization");
62
79
 
@@ -11,3 +11,10 @@ export const DISPATCH_WORKSPACE_SSO_FLAG = defineFeatureFlag({
11
11
  description:
12
12
  "Let Dispatch use the signed-in workspace identity for exact registered app panes.",
13
13
  });
14
+
15
+ export const DISPATCH_WORKSPACE_APP_LIST_FLAG = defineFeatureFlag({
16
+ key: "dispatch.workspace-app-list",
17
+ displayName: "Workspace app list",
18
+ description:
19
+ "Show apps from the signed-in workspace in native desktop and mobile app lists.",
20
+ });