@agent-native/dispatch 0.29.3 → 0.29.4
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/dist/actions/provider-api-register.d.ts +14 -14
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +18 -3
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/workspace-app-card.d.ts.map +1 -1
- package/dist/components/workspace-app-card.js +9 -6
- package/dist/components/workspace-app-card.js.map +1 -1
- package/dist/components/workspace-app-host.d.ts.map +1 -1
- package/dist/components/workspace-app-host.js +48 -8
- package/dist/components/workspace-app-host.js.map +1 -1
- package/dist/lib/workspace-apps.d.ts +16 -0
- package/dist/lib/workspace-apps.d.ts.map +1 -1
- package/dist/lib/workspace-apps.js +71 -1
- package/dist/lib/workspace-apps.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts +2 -0
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +9 -3
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/mcp-gateway.d.ts.map +1 -1
- package/dist/server/lib/mcp-gateway.js +5 -42
- package/dist/server/lib/mcp-gateway.js.map +1 -1
- package/dist/shared/workspace-sso.d.ts +12 -0
- package/dist/shared/workspace-sso.d.ts.map +1 -1
- package/dist/shared/workspace-sso.js +43 -0
- package/dist/shared/workspace-sso.js.map +1 -1
- package/package.json +2 -2
- package/src/components/layout/Layout.tsx +26 -4
- package/src/components/workspace-app-card.spec.tsx +60 -0
- package/src/components/workspace-app-card.tsx +17 -4
- package/src/components/workspace-app-host.spec.tsx +104 -3
- package/src/components/workspace-app-host.tsx +50 -6
- package/src/lib/workspace-apps.spec.ts +80 -0
- package/src/lib/workspace-apps.ts +95 -1
- package/src/server/lib/app-creation-store.spec.ts +43 -0
- package/src/server/lib/app-creation-store.ts +11 -3
- package/src/server/lib/mcp-gateway.ts +5 -48
- 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
|
|
1330
|
-
|
|
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
|
-
|
|
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
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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
|
+
}
|