@agent-native/dispatch 0.40.40 → 0.40.41-beta-dispatch-embed-01a15dc9-20261027050558
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/CHANGELOG.md +42 -0
- package/dist/actions/create_embed_session.js +1 -0
- package/dist/actions/create_embed_session.js.map +1 -1
- package/dist/actions/delete-destination.d.ts +12 -12
- package/dist/actions/provider-api-register.d.ts +13 -13
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +8 -11
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/workspace-app-host.d.ts.map +1 -1
- package/dist/components/workspace-app-host.js +23 -10
- package/dist/components/workspace-app-host.js.map +1 -1
- package/dist/lib/workspace-apps.d.ts +2 -2
- package/dist/lib/workspace-apps.d.ts.map +1 -1
- package/dist/lib/workspace-apps.js +35 -5
- package/dist/lib/workspace-apps.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +173 -48
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/community-apps.js +1 -1
- package/dist/server/lib/community-apps.js.map +1 -1
- package/dist/server/lib/mcp-gateway.d.ts +1 -0
- package/dist/server/lib/mcp-gateway.d.ts.map +1 -1
- package/dist/server/lib/mcp-gateway.js +170 -38
- package/dist/server/lib/mcp-gateway.js.map +1 -1
- package/dist/server/lib/provider-api.d.ts +2 -2
- package/dist/shared/workspace-app-route.d.ts +2 -0
- package/dist/shared/workspace-app-route.d.ts.map +1 -0
- package/dist/shared/workspace-app-route.js +4 -0
- package/dist/shared/workspace-app-route.js.map +1 -0
- package/package.json +4 -4
- package/src/actions/create_embed_session.ts +1 -0
- package/src/actions/index.spec.ts +3 -0
- package/src/components/layout/Layout.spec.tsx +39 -9
- package/src/components/layout/Layout.tsx +19 -40
- package/src/components/workspace-app-host.spec.tsx +88 -1
- package/src/components/workspace-app-host.tsx +30 -9
- package/src/lib/workspace-apps.spec.ts +64 -5
- package/src/lib/workspace-apps.ts +36 -6
- package/src/routes/pages/apps.$appId.spec.tsx +21 -3
- package/src/server/lib/app-creation-store.spec.ts +502 -11
- package/src/server/lib/app-creation-store.ts +198 -50
- package/src/server/lib/community-apps.ts +1 -1
- package/src/server/lib/mcp-gateway.spec.ts +396 -9
- package/src/server/lib/mcp-gateway.ts +209 -45
- package/src/shared/workspace-app-route.ts +3 -0
|
@@ -18,12 +18,14 @@ const mocks = vi.hoisted(() => ({
|
|
|
18
18
|
return `/_agent-native/embed/start?ticket=${encodeURIComponent(ticket)}`;
|
|
19
19
|
}),
|
|
20
20
|
managerStart: vi.fn(),
|
|
21
|
+
managerGetConnectErrorStatus: vi.fn(),
|
|
21
22
|
managerStop: vi.fn(),
|
|
22
23
|
managerCallTool: vi.fn(),
|
|
23
24
|
managerConstructor: vi.fn(),
|
|
24
25
|
a2aConstructor: vi.fn(),
|
|
25
26
|
a2aSend: vi.fn(),
|
|
26
27
|
a2aGetTask: vi.fn(),
|
|
28
|
+
signA2AOrgMemberDelegationToken: vi.fn(),
|
|
27
29
|
signA2AOrganizationToken: vi.fn(),
|
|
28
30
|
signA2AToken: vi.fn(),
|
|
29
31
|
canonicalA2AAudience: vi.fn((url: string) => url.replace(/\/+$/, "")),
|
|
@@ -88,6 +90,7 @@ vi.mock("@agent-native/core/a2a", async (importOriginal) => {
|
|
|
88
90
|
}
|
|
89
91
|
},
|
|
90
92
|
signA2AOrganizationToken: mocks.signA2AOrganizationToken,
|
|
93
|
+
signA2AOrgMemberDelegationToken: mocks.signA2AOrgMemberDelegationToken,
|
|
91
94
|
signA2AToken: mocks.signA2AToken,
|
|
92
95
|
getGlobalA2ASecret: () => process.env.A2A_SECRET?.trim(),
|
|
93
96
|
canonicalA2AAudience: mocks.canonicalA2AAudience,
|
|
@@ -111,6 +114,10 @@ vi.mock("@agent-native/core/mcp-client", () => ({
|
|
|
111
114
|
return mocks.managerStart();
|
|
112
115
|
}
|
|
113
116
|
|
|
117
|
+
getConnectErrorStatus(serverId: string) {
|
|
118
|
+
return mocks.managerGetConnectErrorStatus(serverId);
|
|
119
|
+
}
|
|
120
|
+
|
|
114
121
|
stop() {
|
|
115
122
|
return mocks.managerStop();
|
|
116
123
|
}
|
|
@@ -165,6 +172,7 @@ beforeEach(() => {
|
|
|
165
172
|
mocks.a2aSend.mockReset();
|
|
166
173
|
mocks.a2aGetTask.mockReset();
|
|
167
174
|
mocks.signA2AOrganizationToken.mockReset();
|
|
175
|
+
mocks.signA2AOrgMemberDelegationToken.mockReset();
|
|
168
176
|
mocks.signA2AToken.mockReset();
|
|
169
177
|
mocks.discoverAgents.mockResolvedValue([analyticsAgent]);
|
|
170
178
|
mocks.getBuiltinAgents.mockReturnValue([]);
|
|
@@ -177,6 +185,7 @@ beforeEach(() => {
|
|
|
177
185
|
expiresAt: 12345,
|
|
178
186
|
});
|
|
179
187
|
mocks.managerStart.mockResolvedValue(undefined);
|
|
188
|
+
mocks.managerGetConnectErrorStatus.mockReturnValue(undefined);
|
|
180
189
|
mocks.managerStop.mockResolvedValue(undefined);
|
|
181
190
|
mocks.managerCallTool.mockResolvedValue({
|
|
182
191
|
structuredContent: {
|
|
@@ -194,9 +203,11 @@ beforeEach(() => {
|
|
|
194
203
|
},
|
|
195
204
|
});
|
|
196
205
|
mocks.signA2AToken.mockResolvedValue("signed-token");
|
|
206
|
+
mocks.signA2AOrgMemberDelegationToken.mockResolvedValue("org-member-token");
|
|
197
207
|
mocks.signA2AOrganizationToken.mockResolvedValue("signed-token");
|
|
198
208
|
mocks.getOrgA2ASecret.mockResolvedValue(null);
|
|
199
209
|
mocks.getOrgDomain.mockResolvedValue(null);
|
|
210
|
+
vi.stubEnv("A2A_SECRET", "");
|
|
200
211
|
mocks.isFeatureFlagEnabled.mockResolvedValue(true);
|
|
201
212
|
});
|
|
202
213
|
|
|
@@ -1575,6 +1586,228 @@ describe("createGrantedDispatchMcpEmbedSession", () => {
|
|
|
1575
1586
|
expect(mocks.managerConstructor).toHaveBeenCalled();
|
|
1576
1587
|
});
|
|
1577
1588
|
|
|
1589
|
+
it.each([
|
|
1590
|
+
[
|
|
1591
|
+
"workspace-sso",
|
|
1592
|
+
"gateway mount path",
|
|
1593
|
+
"https://beta.agent-workspace.builder.io/dispatch",
|
|
1594
|
+
"",
|
|
1595
|
+
],
|
|
1596
|
+
[
|
|
1597
|
+
"dispatch-grant",
|
|
1598
|
+
"gateway mount path",
|
|
1599
|
+
"https://beta.agent-workspace.builder.io/dispatch",
|
|
1600
|
+
"",
|
|
1601
|
+
],
|
|
1602
|
+
[
|
|
1603
|
+
"workspace-sso",
|
|
1604
|
+
"APP_BASE_PATH",
|
|
1605
|
+
"https://beta.agent-workspace.builder.io",
|
|
1606
|
+
"/dispatch",
|
|
1607
|
+
],
|
|
1608
|
+
[
|
|
1609
|
+
"dispatch-grant",
|
|
1610
|
+
"APP_BASE_PATH",
|
|
1611
|
+
"https://beta.agent-workspace.builder.io",
|
|
1612
|
+
"/dispatch",
|
|
1613
|
+
],
|
|
1614
|
+
] as const)(
|
|
1615
|
+
"uses the mounted Workspace MCP endpoint and audience for %s embeds with %s",
|
|
1616
|
+
async (route, _mountConfig, gatewayUrl, appBasePath) => {
|
|
1617
|
+
vi.stubEnv("A2A_SECRET", "test-a2a-secret");
|
|
1618
|
+
vi.stubEnv("WORKSPACE_GATEWAY_URL", gatewayUrl);
|
|
1619
|
+
vi.stubEnv("APP_BASE_PATH", appBasePath);
|
|
1620
|
+
vi.stubEnv("VITE_APP_BASE_PATH", "");
|
|
1621
|
+
vi.stubEnv(
|
|
1622
|
+
"IDENTITY_SSO_APP_REGISTRY_JSON",
|
|
1623
|
+
JSON.stringify([
|
|
1624
|
+
{
|
|
1625
|
+
appId: "customer-ticket-triage",
|
|
1626
|
+
clientId: "workspace-client",
|
|
1627
|
+
origin: "https://beta.agent-workspace.builder.io",
|
|
1628
|
+
callbackPath: "/_agent-native/identity/callback",
|
|
1629
|
+
capabilities: ["identity-sso"],
|
|
1630
|
+
},
|
|
1631
|
+
]),
|
|
1632
|
+
);
|
|
1633
|
+
const workspaceApp = {
|
|
1634
|
+
id: "customer-ticket-triage",
|
|
1635
|
+
name: "Customer Ticket Triage",
|
|
1636
|
+
description: "Triage customer tickets",
|
|
1637
|
+
path: "/customer-ticket-triage",
|
|
1638
|
+
url: "https://beta.agent-workspace.builder.io/dispatch/apps/customer-ticket-triage",
|
|
1639
|
+
isDispatch: false,
|
|
1640
|
+
};
|
|
1641
|
+
mocks.listWorkspaceApps.mockResolvedValue([workspaceApp]);
|
|
1642
|
+
mocks.discoverAgents.mockResolvedValue([
|
|
1643
|
+
{
|
|
1644
|
+
id: workspaceApp.id,
|
|
1645
|
+
name: workspaceApp.name,
|
|
1646
|
+
description: workspaceApp.description,
|
|
1647
|
+
url: workspaceApp.url,
|
|
1648
|
+
color: "#14B8A6",
|
|
1649
|
+
},
|
|
1650
|
+
]);
|
|
1651
|
+
mocks.managerCallTool.mockResolvedValueOnce({
|
|
1652
|
+
structuredContent: {
|
|
1653
|
+
startUrl:
|
|
1654
|
+
"https://beta.agent-workspace.builder.io/dispatch/_agent-native/embed/start?ticket=remote",
|
|
1655
|
+
},
|
|
1656
|
+
});
|
|
1657
|
+
|
|
1658
|
+
const result = await runWithRequestContext(
|
|
1659
|
+
{
|
|
1660
|
+
userEmail: "owner@example.test",
|
|
1661
|
+
requestOrigin: "https://beta.dispatch.agent-native.com",
|
|
1662
|
+
},
|
|
1663
|
+
() =>
|
|
1664
|
+
route === "workspace-sso"
|
|
1665
|
+
? createWorkspaceSsoEmbedSession({
|
|
1666
|
+
app: workspaceApp.id,
|
|
1667
|
+
path: "/triage",
|
|
1668
|
+
chrome: "minimal",
|
|
1669
|
+
})
|
|
1670
|
+
: createGrantedDispatchMcpEmbedSession({
|
|
1671
|
+
app: workspaceApp.id,
|
|
1672
|
+
path: "/triage",
|
|
1673
|
+
chrome: "minimal",
|
|
1674
|
+
}),
|
|
1675
|
+
);
|
|
1676
|
+
|
|
1677
|
+
expect(mocks.managerConstructor).toHaveBeenCalledWith(
|
|
1678
|
+
expect.objectContaining({
|
|
1679
|
+
servers: {
|
|
1680
|
+
target: expect.objectContaining({
|
|
1681
|
+
url: "https://beta.agent-workspace.builder.io/dispatch/mcp",
|
|
1682
|
+
headers: { Authorization: "Bearer signed-token" },
|
|
1683
|
+
}),
|
|
1684
|
+
},
|
|
1685
|
+
}),
|
|
1686
|
+
);
|
|
1687
|
+
expect(mocks.signA2AToken).toHaveBeenCalledWith(
|
|
1688
|
+
"owner@example.test",
|
|
1689
|
+
undefined,
|
|
1690
|
+
undefined,
|
|
1691
|
+
expect.objectContaining({
|
|
1692
|
+
audience: "https://beta.agent-workspace.builder.io/dispatch/mcp",
|
|
1693
|
+
preferGlobalSecret: true,
|
|
1694
|
+
}),
|
|
1695
|
+
);
|
|
1696
|
+
expect(mocks.managerCallTool).toHaveBeenCalledWith(
|
|
1697
|
+
"mcp__target__create_embed_session",
|
|
1698
|
+
{
|
|
1699
|
+
url: "https://beta.agent-workspace.builder.io/dispatch/apps/customer-ticket-triage/triage",
|
|
1700
|
+
chrome: "minimal",
|
|
1701
|
+
},
|
|
1702
|
+
);
|
|
1703
|
+
expect(result.startUrl).toBe(
|
|
1704
|
+
"https://beta.agent-workspace.builder.io/dispatch/_agent-native/embed/start?ticket=remote",
|
|
1705
|
+
);
|
|
1706
|
+
|
|
1707
|
+
mocks.managerCallTool.mockResolvedValueOnce({
|
|
1708
|
+
structuredContent: {
|
|
1709
|
+
startUrl:
|
|
1710
|
+
"https://beta.agent-workspace.builder.io/dispatch/apps/customer-ticket-triage/_agent-native/embed/start?ticket=sibling",
|
|
1711
|
+
},
|
|
1712
|
+
});
|
|
1713
|
+
await expect(
|
|
1714
|
+
runWithRequestContext(
|
|
1715
|
+
{
|
|
1716
|
+
userEmail: "owner@example.test",
|
|
1717
|
+
requestOrigin: "https://beta.dispatch.agent-native.com",
|
|
1718
|
+
},
|
|
1719
|
+
() =>
|
|
1720
|
+
route === "workspace-sso"
|
|
1721
|
+
? createWorkspaceSsoEmbedSession({
|
|
1722
|
+
app: workspaceApp.id,
|
|
1723
|
+
path: "/",
|
|
1724
|
+
chrome: "minimal",
|
|
1725
|
+
})
|
|
1726
|
+
: createGrantedDispatchMcpEmbedSession({
|
|
1727
|
+
app: workspaceApp.id,
|
|
1728
|
+
path: "/",
|
|
1729
|
+
chrome: "minimal",
|
|
1730
|
+
}),
|
|
1731
|
+
),
|
|
1732
|
+
).rejects.toThrow(/invalid embed start URL/);
|
|
1733
|
+
},
|
|
1734
|
+
);
|
|
1735
|
+
|
|
1736
|
+
it("does not fall back when the configured Workspace gateway URL is invalid", async () => {
|
|
1737
|
+
vi.stubEnv("WORKSPACE_GATEWAY_URL", "not-a-url");
|
|
1738
|
+
mocks.listWorkspaceApps.mockResolvedValue([
|
|
1739
|
+
{
|
|
1740
|
+
id: "customer-ticket-triage",
|
|
1741
|
+
name: "Customer Ticket Triage",
|
|
1742
|
+
description: "Triage customer tickets",
|
|
1743
|
+
path: "/customer-ticket-triage",
|
|
1744
|
+
url: "https://beta.agent-workspace.builder.io/dispatch/apps/customer-ticket-triage",
|
|
1745
|
+
isDispatch: false,
|
|
1746
|
+
},
|
|
1747
|
+
]);
|
|
1748
|
+
|
|
1749
|
+
await expect(
|
|
1750
|
+
runWithRequestContext(
|
|
1751
|
+
{
|
|
1752
|
+
userEmail: "owner@example.test",
|
|
1753
|
+
requestOrigin: "https://beta.dispatch.agent-native.com",
|
|
1754
|
+
},
|
|
1755
|
+
() =>
|
|
1756
|
+
createWorkspaceSsoEmbedSession({
|
|
1757
|
+
app: "customer-ticket-triage",
|
|
1758
|
+
path: "/",
|
|
1759
|
+
chrome: "minimal",
|
|
1760
|
+
}),
|
|
1761
|
+
),
|
|
1762
|
+
).rejects.toThrow();
|
|
1763
|
+
expect(mocks.managerConstructor).not.toHaveBeenCalled();
|
|
1764
|
+
});
|
|
1765
|
+
|
|
1766
|
+
it("rejects unsupported Workspace gateway schemes before beta projection", async () => {
|
|
1767
|
+
vi.stubEnv("A2A_SECRET", "test-a2a-secret");
|
|
1768
|
+
vi.stubEnv(
|
|
1769
|
+
"WORKSPACE_GATEWAY_URL",
|
|
1770
|
+
"ftp://agent-workspace.builder.io/dispatch",
|
|
1771
|
+
);
|
|
1772
|
+
vi.stubEnv("APP_BASE_PATH", "");
|
|
1773
|
+
vi.stubEnv("VITE_APP_BASE_PATH", "");
|
|
1774
|
+
const workspaceApp = {
|
|
1775
|
+
id: "customer-ticket-triage",
|
|
1776
|
+
name: "Customer Ticket Triage",
|
|
1777
|
+
description: "Triage customer tickets",
|
|
1778
|
+
path: "/customer-ticket-triage",
|
|
1779
|
+
url: "https://beta.agent-workspace.builder.io/dispatch/apps/customer-ticket-triage",
|
|
1780
|
+
isDispatch: false,
|
|
1781
|
+
};
|
|
1782
|
+
mocks.listWorkspaceApps.mockResolvedValue([workspaceApp]);
|
|
1783
|
+
mocks.discoverAgents.mockResolvedValue([
|
|
1784
|
+
{
|
|
1785
|
+
id: workspaceApp.id,
|
|
1786
|
+
name: workspaceApp.name,
|
|
1787
|
+
description: workspaceApp.description,
|
|
1788
|
+
url: workspaceApp.url,
|
|
1789
|
+
color: "#14B8A6",
|
|
1790
|
+
},
|
|
1791
|
+
]);
|
|
1792
|
+
|
|
1793
|
+
await expect(
|
|
1794
|
+
runWithRequestContext(
|
|
1795
|
+
{
|
|
1796
|
+
userEmail: "owner@example.test",
|
|
1797
|
+
requestOrigin: "https://beta.dispatch.agent-native.com",
|
|
1798
|
+
},
|
|
1799
|
+
() =>
|
|
1800
|
+
createGrantedDispatchMcpEmbedSession({
|
|
1801
|
+
app: workspaceApp.id,
|
|
1802
|
+
path: "/",
|
|
1803
|
+
chrome: "minimal",
|
|
1804
|
+
}),
|
|
1805
|
+
),
|
|
1806
|
+
).rejects.toThrow("WORKSPACE_GATEWAY_URL must use http or https.");
|
|
1807
|
+
expect(mocks.signA2AToken).not.toHaveBeenCalled();
|
|
1808
|
+
expect(mocks.managerConstructor).not.toHaveBeenCalled();
|
|
1809
|
+
});
|
|
1810
|
+
|
|
1578
1811
|
it("rejects beta workspace SSO apps on the production lane", async () => {
|
|
1579
1812
|
mocks.discoverAgents.mockResolvedValue([]);
|
|
1580
1813
|
mocks.listWorkspaceApps.mockResolvedValue([
|
|
@@ -2015,7 +2248,9 @@ describe("createGrantedDispatchMcpEmbedSession", () => {
|
|
|
2015
2248
|
});
|
|
2016
2249
|
});
|
|
2017
2250
|
|
|
2018
|
-
it("
|
|
2251
|
+
it("does not send the source app's local org ID to the target", async () => {
|
|
2252
|
+
mocks.getOrgDomain.mockResolvedValue("builder.io");
|
|
2253
|
+
|
|
2019
2254
|
await runWithRequestContext(
|
|
2020
2255
|
{
|
|
2021
2256
|
userEmail: "owner@example.test",
|
|
@@ -2037,11 +2272,10 @@ describe("createGrantedDispatchMcpEmbedSession", () => {
|
|
|
2037
2272
|
expiresIn: "5m",
|
|
2038
2273
|
audience: "http://localhost:8086/mcp",
|
|
2039
2274
|
preferGlobalSecret: true,
|
|
2040
|
-
extraClaims: { org_id: "org-1" },
|
|
2041
2275
|
},
|
|
2042
2276
|
);
|
|
2043
2277
|
expect(mocks.signA2AOrganizationToken).not.toHaveBeenCalled();
|
|
2044
|
-
expect(mocks.getOrgDomain).
|
|
2278
|
+
expect(mocks.getOrgDomain).toHaveBeenCalledWith("org-1");
|
|
2045
2279
|
expect(mocks.getOrgA2ASecret).not.toHaveBeenCalled();
|
|
2046
2280
|
expect(mocks.managerConstructor).toHaveBeenCalledWith(
|
|
2047
2281
|
expect.objectContaining({
|
|
@@ -2054,11 +2288,139 @@ describe("createGrantedDispatchMcpEmbedSession", () => {
|
|
|
2054
2288
|
);
|
|
2055
2289
|
});
|
|
2056
2290
|
|
|
2057
|
-
it("
|
|
2291
|
+
it("uses a user delegation token when only the org secret is configured", async () => {
|
|
2058
2292
|
vi.stubEnv("A2A_SECRET", "");
|
|
2059
2293
|
mocks.getOrgDomain.mockResolvedValue("builder.io");
|
|
2060
2294
|
mocks.getOrgA2ASecret.mockResolvedValue("org-specific-secret");
|
|
2061
2295
|
|
|
2296
|
+
const result = await runWithRequestContext(
|
|
2297
|
+
{
|
|
2298
|
+
userEmail: "owner@example.test",
|
|
2299
|
+
orgId: "org-1",
|
|
2300
|
+
requestOrigin: "http://localhost:8092",
|
|
2301
|
+
},
|
|
2302
|
+
() =>
|
|
2303
|
+
createGrantedDispatchMcpEmbedSession({
|
|
2304
|
+
app: "analytics",
|
|
2305
|
+
path: "/dashboards",
|
|
2306
|
+
}),
|
|
2307
|
+
);
|
|
2308
|
+
|
|
2309
|
+
expect(mocks.signA2AToken).not.toHaveBeenCalled();
|
|
2310
|
+
expect(mocks.signA2AOrgMemberDelegationToken).toHaveBeenCalledWith(
|
|
2311
|
+
"owner@example.test",
|
|
2312
|
+
"builder.io",
|
|
2313
|
+
"org-specific-secret",
|
|
2314
|
+
"http://localhost:8086/mcp",
|
|
2315
|
+
);
|
|
2316
|
+
expect(mocks.signA2AOrganizationToken).not.toHaveBeenCalled();
|
|
2317
|
+
expect(result.startUrl).toBe(
|
|
2318
|
+
"http://localhost:8086/_agent-native/embed/start?ticket=remote",
|
|
2319
|
+
);
|
|
2320
|
+
expect(mocks.managerConstructor).toHaveBeenCalledWith(
|
|
2321
|
+
expect.objectContaining({
|
|
2322
|
+
servers: {
|
|
2323
|
+
target: expect.objectContaining({
|
|
2324
|
+
headers: { Authorization: "Bearer org-member-token" },
|
|
2325
|
+
}),
|
|
2326
|
+
},
|
|
2327
|
+
}),
|
|
2328
|
+
);
|
|
2329
|
+
});
|
|
2330
|
+
|
|
2331
|
+
it("tries org-member delegation after a deployment-token auth rejection", async () => {
|
|
2332
|
+
mocks.getOrgDomain.mockResolvedValue("builder.io");
|
|
2333
|
+
mocks.getOrgA2ASecret.mockResolvedValue("org-specific-secret");
|
|
2334
|
+
mocks.signA2AToken.mockResolvedValue("deployment-user-token");
|
|
2335
|
+
mocks.signA2AOrgMemberDelegationToken.mockResolvedValue("org-member-token");
|
|
2336
|
+
mocks.managerGetConnectErrorStatus.mockReturnValueOnce(401);
|
|
2337
|
+
|
|
2338
|
+
const result = await runWithRequestContext(
|
|
2339
|
+
{
|
|
2340
|
+
userEmail: "owner@example.test",
|
|
2341
|
+
orgId: "org-1",
|
|
2342
|
+
requestOrigin: "http://localhost:8092",
|
|
2343
|
+
},
|
|
2344
|
+
() =>
|
|
2345
|
+
createGrantedDispatchMcpEmbedSession({
|
|
2346
|
+
app: "analytics",
|
|
2347
|
+
path: "/dashboards",
|
|
2348
|
+
}),
|
|
2349
|
+
);
|
|
2350
|
+
|
|
2351
|
+
expect(result.startUrl).toBe(
|
|
2352
|
+
"http://localhost:8086/_agent-native/embed/start?ticket=remote",
|
|
2353
|
+
);
|
|
2354
|
+
expect(mocks.managerConstructor).toHaveBeenNthCalledWith(
|
|
2355
|
+
1,
|
|
2356
|
+
expect.objectContaining({
|
|
2357
|
+
servers: {
|
|
2358
|
+
target: expect.objectContaining({
|
|
2359
|
+
headers: { Authorization: "Bearer deployment-user-token" },
|
|
2360
|
+
}),
|
|
2361
|
+
},
|
|
2362
|
+
}),
|
|
2363
|
+
);
|
|
2364
|
+
expect(mocks.managerConstructor).toHaveBeenNthCalledWith(
|
|
2365
|
+
2,
|
|
2366
|
+
expect.objectContaining({
|
|
2367
|
+
servers: {
|
|
2368
|
+
target: expect.objectContaining({
|
|
2369
|
+
headers: { Authorization: "Bearer org-member-token" },
|
|
2370
|
+
}),
|
|
2371
|
+
},
|
|
2372
|
+
}),
|
|
2373
|
+
);
|
|
2374
|
+
expect(mocks.managerCallTool).toHaveBeenCalledTimes(1);
|
|
2375
|
+
expect(mocks.signA2AOrgMemberDelegationToken).toHaveBeenCalledWith(
|
|
2376
|
+
"owner@example.test",
|
|
2377
|
+
"builder.io",
|
|
2378
|
+
"org-specific-secret",
|
|
2379
|
+
"http://localhost:8086/mcp",
|
|
2380
|
+
);
|
|
2381
|
+
});
|
|
2382
|
+
|
|
2383
|
+
it("uses member delegation when the org secret matches the source deployment secret", async () => {
|
|
2384
|
+
vi.stubEnv("A2A_SECRET", "source-deployment-secret");
|
|
2385
|
+
mocks.getOrgDomain.mockResolvedValue("builder.io");
|
|
2386
|
+
mocks.getOrgA2ASecret.mockResolvedValue("source-deployment-secret");
|
|
2387
|
+
mocks.managerGetConnectErrorStatus.mockReturnValueOnce(401);
|
|
2388
|
+
|
|
2389
|
+
const result = await runWithRequestContext(
|
|
2390
|
+
{
|
|
2391
|
+
userEmail: "owner@example.test",
|
|
2392
|
+
orgId: "org-1",
|
|
2393
|
+
requestOrigin: "http://localhost:8092",
|
|
2394
|
+
},
|
|
2395
|
+
() =>
|
|
2396
|
+
createGrantedDispatchMcpEmbedSession({
|
|
2397
|
+
app: "analytics",
|
|
2398
|
+
path: "/dashboards",
|
|
2399
|
+
}),
|
|
2400
|
+
);
|
|
2401
|
+
|
|
2402
|
+
expect(result.startUrl).toBe(
|
|
2403
|
+
"http://localhost:8086/_agent-native/embed/start?ticket=remote",
|
|
2404
|
+
);
|
|
2405
|
+
expect(mocks.managerConstructor).toHaveBeenCalledTimes(2);
|
|
2406
|
+
expect(mocks.signA2AOrgMemberDelegationToken).toHaveBeenCalledWith(
|
|
2407
|
+
"owner@example.test",
|
|
2408
|
+
"builder.io",
|
|
2409
|
+
"source-deployment-secret",
|
|
2410
|
+
"http://localhost:8086/mcp",
|
|
2411
|
+
);
|
|
2412
|
+
});
|
|
2413
|
+
|
|
2414
|
+
it("does not treat a bare status number in an error message as an auth refusal", async () => {
|
|
2415
|
+
vi.stubEnv("A2A_SECRET", "source-deployment-secret");
|
|
2416
|
+
mocks.getOrgDomain.mockResolvedValue("builder.io");
|
|
2417
|
+
mocks.getOrgA2ASecret.mockResolvedValue("org-specific-secret");
|
|
2418
|
+
mocks.managerStart.mockRejectedValueOnce(
|
|
2419
|
+
new Error(
|
|
2420
|
+
"MCP response body mentions reference 403 from an unrelated request",
|
|
2421
|
+
),
|
|
2422
|
+
);
|
|
2423
|
+
|
|
2062
2424
|
await expect(
|
|
2063
2425
|
runWithRequestContext(
|
|
2064
2426
|
{
|
|
@@ -2072,13 +2434,38 @@ describe("createGrantedDispatchMcpEmbedSession", () => {
|
|
|
2072
2434
|
path: "/dashboards",
|
|
2073
2435
|
}),
|
|
2074
2436
|
),
|
|
2075
|
-
).rejects.toThrow(
|
|
2076
|
-
|
|
2437
|
+
).rejects.toThrow(/reference 403 from an unrelated request/);
|
|
2438
|
+
|
|
2439
|
+
expect(mocks.managerConstructor).toHaveBeenCalledTimes(1);
|
|
2440
|
+
expect(mocks.signA2AOrgMemberDelegationToken).not.toHaveBeenCalled();
|
|
2441
|
+
expect(mocks.getOrgA2ASecret).not.toHaveBeenCalled();
|
|
2442
|
+
});
|
|
2443
|
+
|
|
2444
|
+
it("does not retry an action-level authorization error with member delegation", async () => {
|
|
2445
|
+
mocks.getOrgDomain.mockResolvedValue("builder.io");
|
|
2446
|
+
mocks.getOrgA2ASecret.mockResolvedValue("org-specific-secret");
|
|
2447
|
+
mocks.managerCallTool.mockRejectedValueOnce(
|
|
2448
|
+
Object.assign(new Error("Forbidden"), { status: 403 }),
|
|
2077
2449
|
);
|
|
2078
2450
|
|
|
2079
|
-
expect(
|
|
2080
|
-
|
|
2081
|
-
|
|
2451
|
+
await expect(
|
|
2452
|
+
runWithRequestContext(
|
|
2453
|
+
{
|
|
2454
|
+
userEmail: "owner@example.test",
|
|
2455
|
+
orgId: "org-1",
|
|
2456
|
+
requestOrigin: "http://localhost:8092",
|
|
2457
|
+
},
|
|
2458
|
+
() =>
|
|
2459
|
+
createGrantedDispatchMcpEmbedSession({
|
|
2460
|
+
app: "analytics",
|
|
2461
|
+
path: "/dashboards",
|
|
2462
|
+
}),
|
|
2463
|
+
),
|
|
2464
|
+
).rejects.toThrow("Forbidden");
|
|
2465
|
+
|
|
2466
|
+
expect(mocks.managerConstructor).toHaveBeenCalledTimes(1);
|
|
2467
|
+
expect(mocks.signA2AOrgMemberDelegationToken).not.toHaveBeenCalled();
|
|
2468
|
+
expect(mocks.getOrgA2ASecret).not.toHaveBeenCalled();
|
|
2082
2469
|
});
|
|
2083
2470
|
|
|
2084
2471
|
it("does not retry an authenticated-caller error with an org principal", async () => {
|