@agent-native/dispatch 0.29.3 → 0.29.5

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 (37) hide show
  1. package/dist/actions/provider-api-register.d.ts +4 -4
  2. package/dist/components/layout/Layout.d.ts.map +1 -1
  3. package/dist/components/layout/Layout.js +18 -3
  4. package/dist/components/layout/Layout.js.map +1 -1
  5. package/dist/components/workspace-app-card.d.ts.map +1 -1
  6. package/dist/components/workspace-app-card.js +9 -6
  7. package/dist/components/workspace-app-card.js.map +1 -1
  8. package/dist/components/workspace-app-host.d.ts.map +1 -1
  9. package/dist/components/workspace-app-host.js +48 -8
  10. package/dist/components/workspace-app-host.js.map +1 -1
  11. package/dist/lib/workspace-apps.d.ts +16 -0
  12. package/dist/lib/workspace-apps.d.ts.map +1 -1
  13. package/dist/lib/workspace-apps.js +71 -1
  14. package/dist/lib/workspace-apps.js.map +1 -1
  15. package/dist/server/lib/app-creation-store.d.ts +2 -0
  16. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  17. package/dist/server/lib/app-creation-store.js +9 -3
  18. package/dist/server/lib/app-creation-store.js.map +1 -1
  19. package/dist/server/lib/mcp-gateway.d.ts.map +1 -1
  20. package/dist/server/lib/mcp-gateway.js +5 -42
  21. package/dist/server/lib/mcp-gateway.js.map +1 -1
  22. package/dist/shared/workspace-sso.d.ts +12 -0
  23. package/dist/shared/workspace-sso.d.ts.map +1 -1
  24. package/dist/shared/workspace-sso.js +43 -0
  25. package/dist/shared/workspace-sso.js.map +1 -1
  26. package/package.json +3 -3
  27. package/src/components/layout/Layout.tsx +26 -4
  28. package/src/components/workspace-app-card.spec.tsx +60 -0
  29. package/src/components/workspace-app-card.tsx +17 -4
  30. package/src/components/workspace-app-host.spec.tsx +104 -3
  31. package/src/components/workspace-app-host.tsx +50 -6
  32. package/src/lib/workspace-apps.spec.ts +80 -0
  33. package/src/lib/workspace-apps.ts +95 -1
  34. package/src/server/lib/app-creation-store.spec.ts +43 -0
  35. package/src/server/lib/app-creation-store.ts +11 -3
  36. package/src/server/lib/mcp-gateway.ts +5 -48
  37. package/src/shared/workspace-sso.ts +55 -0
@@ -345,6 +345,49 @@ describe("listWorkspaceApps", () => {
345
345
  expect(apps.map((app) => app.id)).toEqual(["dispatch"]);
346
346
  });
347
347
 
348
+ it("projects exact custom SSO eligibility without exposing registry details", async () => {
349
+ stubNoPendingContext();
350
+ stubManifest([
351
+ { id: "dispatch", name: "Dispatch", path: "/dispatch" },
352
+ {
353
+ id: "workspace-reports",
354
+ name: "Workspace Reports",
355
+ path: "/workspace-reports",
356
+ url: "https://reports.example.com/workspace-reports",
357
+ },
358
+ {
359
+ id: "unregistered",
360
+ name: "Unregistered",
361
+ path: "/unregistered",
362
+ url: "https://unregistered.example.com",
363
+ },
364
+ ]);
365
+ vi.stubEnv(
366
+ "IDENTITY_SSO_APP_REGISTRY_JSON",
367
+ JSON.stringify([
368
+ {
369
+ appId: "workspace-reports",
370
+ clientId: "workspace-reports-client",
371
+ origin: "https://reports.example.com",
372
+ callbackPath: "/_agent-native/identity/callback",
373
+ capabilities: ["identity-sso"],
374
+ },
375
+ ]),
376
+ );
377
+
378
+ const apps = await runWithRequestContext(
379
+ { userEmail: "dev@example.test" },
380
+ () => listWorkspaceApps({ includeAgentCards: false }),
381
+ );
382
+
383
+ expect(apps.find((app) => app.id === "workspace-reports")).toMatchObject({
384
+ workspaceSso: true,
385
+ });
386
+ expect(apps.find((app) => app.id === "unregistered")).toMatchObject({
387
+ workspaceSso: false,
388
+ });
389
+ });
390
+
348
391
  it("filters workspace apps by audience", async () => {
349
392
  stubNoPendingContext();
350
393
  vi.stubEnv(
@@ -17,6 +17,7 @@ import {
17
17
  import { getSetting, putSetting } from "@agent-native/core/settings";
18
18
  import { assertValidWorkspaceAppId } from "@agent-native/core/shared";
19
19
 
20
+ import { isWorkspaceSsoAppUrl } from "../../shared/workspace-sso.js";
20
21
  import { identityKeyForIncoming } from "./dispatch-integrations.js";
21
22
  import {
22
23
  currentOrgId,
@@ -103,6 +104,8 @@ export interface WorkspaceAppSummary {
103
104
  agentName?: string | null;
104
105
  agentSkillsCount?: number | null;
105
106
  archived?: boolean;
107
+ /** Safe server-side eligibility projection for the workspace SSO action. */
108
+ workspaceSso?: boolean;
106
109
  }
107
110
 
108
111
  export interface ListWorkspaceAppsOptions {
@@ -1326,9 +1329,14 @@ async function applyArchivedAndPending(
1326
1329
  app,
1327
1330
  metadataSettings,
1328
1331
  );
1329
- return archivedSet.has(app.id)
1330
- ? { ...withMetadata, archived: true }
1331
- : withMetadata;
1332
+ return {
1333
+ ...withMetadata,
1334
+ workspaceSso: isWorkspaceSsoAppUrl(withMetadata, {
1335
+ nodeEnv: process.env.NODE_ENV,
1336
+ registryRaw: process.env.IDENTITY_SSO_APP_REGISTRY_JSON,
1337
+ }),
1338
+ ...(archivedSet.has(app.id) ? { archived: true } : {}),
1339
+ };
1332
1340
  });
1333
1341
  return options.includeArchived
1334
1342
  ? filterAppsByAudience(annotated, options.audience)
@@ -25,9 +25,8 @@ import {
25
25
  } from "@agent-native/core/server/agent-discovery";
26
26
 
27
27
  import {
28
- CANONICAL_WORKSPACE_SSO_APP_ORIGINS,
29
28
  DISPATCH_WORKSPACE_SSO_FLAG,
30
- parseWorkspaceSsoAppRegistrations,
29
+ isWorkspaceSsoAppUrl,
31
30
  } from "../../shared/workspace-sso.js";
32
31
  import {
33
32
  listWorkspaceApps,
@@ -465,17 +464,6 @@ function safeAppOrigin(app: DispatchMcpAccessibleApp): string | null {
465
464
  }
466
465
  }
467
466
 
468
- function isLoopbackOrigin(origin: string): boolean {
469
- try {
470
- const hostname = new URL(origin).hostname;
471
- return ["localhost", "127.0.0.1", "::1"].includes(hostname);
472
- } catch {
473
- // coercion-ok: only a previously validated app origin reaches this helper;
474
- // malformed values are ineligible for the loopback development exception.
475
- return false;
476
- }
477
- }
478
-
479
467
  function appBaseUrl(app: DispatchMcpAccessibleApp): string {
480
468
  return app.url.replace(/\/+$/, "");
481
469
  }
@@ -643,44 +631,13 @@ export async function resolveGrantedDispatchMcpApp(
643
631
  return match;
644
632
  }
645
633
 
646
- function workspaceSsoOriginForApp(appId: string): string | null {
647
- const canonical =
648
- CANONICAL_WORKSPACE_SSO_APP_ORIGINS[
649
- appId as keyof typeof CANONICAL_WORKSPACE_SSO_APP_ORIGINS
650
- ];
651
- if (canonical) return canonical;
652
- const custom = parseWorkspaceSsoAppRegistrations(
653
- process.env.IDENTITY_SSO_APP_REGISTRY_JSON,
654
- ).find((registration) => registration.appId === appId);
655
- return custom?.origin ?? null;
656
- }
657
-
658
- function isWorkspaceSsoCanonicalApp(app: DispatchMcpAccessibleApp): boolean {
659
- const origin = safeAppOrigin(app);
660
- if (!origin) return false;
661
- const registeredOrigin = workspaceSsoOriginForApp(app.id);
662
- if (origin === registeredOrigin) return true;
663
- // The built-in discovery table intentionally points at local dev ports when
664
- // running outside production. Keep local development usable without ever
665
- // weakening the production exact-origin check.
666
- return process.env.NODE_ENV !== "production" && isLoopbackOrigin(origin)
667
- ? Object.prototype.hasOwnProperty.call(
668
- CANONICAL_WORKSPACE_SSO_APP_ORIGINS,
669
- app.id,
670
- )
671
- : false;
672
- }
673
-
674
634
  async function isEligibleWorkspaceSsoApp(
675
635
  candidate: DispatchMcpAccessibleApp,
676
636
  ): Promise<boolean> {
677
- const origin = safeAppOrigin(candidate);
678
- if (!origin) return false;
679
- if (isWorkspaceSsoCanonicalApp(candidate)) return true;
680
-
681
- const customOrigin = workspaceSsoOriginForApp(candidate.id);
682
- if (customOrigin === origin) return true;
683
- return false;
637
+ return isWorkspaceSsoAppUrl(candidate, {
638
+ nodeEnv: process.env.NODE_ENV,
639
+ registryRaw: process.env.IDENTITY_SSO_APP_REGISTRY_JSON,
640
+ });
684
641
  }
685
642
 
686
643
  async function listWorkspaceSsoApps(): Promise<DispatchMcpAccessibleApp[]> {
@@ -116,3 +116,58 @@ export function parseWorkspaceSsoAppRegistrations(
116
116
  }
117
117
  return registrations;
118
118
  }
119
+
120
+ function appOrigin(rawUrl: unknown): string | null {
121
+ if (typeof rawUrl !== "string" || !rawUrl.trim()) return null;
122
+ try {
123
+ const url = new URL(rawUrl.trim());
124
+ return url.protocol === "http:" || url.protocol === "https:"
125
+ ? url.origin
126
+ : null;
127
+ } catch {
128
+ // coercion-ok: malformed app metadata is not eligible for workspace SSO.
129
+ return null;
130
+ }
131
+ }
132
+
133
+ function isLoopbackOrigin(origin: string): boolean {
134
+ try {
135
+ return LOCALHOST_HOSTS.has(new URL(origin).hostname);
136
+ } catch {
137
+ // coercion-ok: only a previously parsed origin reaches this helper.
138
+ return false;
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Keep the server-side app catalog and the browser's action choice on the
144
+ * same exact-origin rules. `registryRaw` is deliberately supplied by the
145
+ * caller so this shared module never reads process or browser environment.
146
+ */
147
+ export function isWorkspaceSsoAppUrl(
148
+ app: { id: string; url?: unknown },
149
+ options: { nodeEnv?: string; registryRaw?: unknown } = {},
150
+ ): boolean {
151
+ const origin = appOrigin(app.url);
152
+ if (!origin) return false;
153
+
154
+ const appId = app.id.trim().toLowerCase();
155
+ const canonicalOrigin =
156
+ CANONICAL_WORKSPACE_SSO_APP_ORIGINS[
157
+ appId as keyof typeof CANONICAL_WORKSPACE_SSO_APP_ORIGINS
158
+ ];
159
+ if (canonicalOrigin === origin) return true;
160
+
161
+ if (
162
+ options.nodeEnv !== "production" &&
163
+ canonicalOrigin &&
164
+ isLoopbackOrigin(origin)
165
+ ) {
166
+ return true;
167
+ }
168
+
169
+ return parseWorkspaceSsoAppRegistrations(options.registryRaw).some(
170
+ (registration) =>
171
+ registration.appId === appId && registration.origin === origin,
172
+ );
173
+ }