@agent-native/dispatch 0.29.5 → 0.30.1
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/create-workspace-resource.d.ts +13 -13
- package/dist/actions/delete-workspace-resource.d.ts +13 -13
- package/dist/actions/provider-api-register.d.ts +8 -8
- package/dist/actions/update-workspace-resource.d.ts +13 -13
- package/dist/components/workspace-app-card.d.ts.map +1 -1
- package/dist/components/workspace-app-card.js +2 -1
- package/dist/components/workspace-app-card.js.map +1 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +26 -1
- package/dist/db/index.js.map +1 -1
- package/dist/db/schema.d.ts +1 -0
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +4 -0
- package/dist/db/schema.js.map +1 -1
- package/dist/lib/workspace-apps.d.ts +1 -0
- package/dist/lib/workspace-apps.d.ts.map +1 -1
- package/dist/lib/workspace-apps.js +3 -1
- package/dist/lib/workspace-apps.js.map +1 -1
- package/dist/lib/workspace-apps.test.js +17 -3
- package/dist/lib/workspace-apps.test.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts +3 -0
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +206 -11
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/package.json +3 -3
- package/src/components/dispatch-control-plane.spec.tsx +17 -3
- package/src/components/workspace-app-card.spec.tsx +17 -0
- package/src/components/workspace-app-card.tsx +12 -0
- package/src/components/workspace-app-host.spec.tsx +14 -6
- package/src/db/index.ts +28 -1
- package/src/db/schema.ts +5 -0
- package/src/lib/workspace-apps.test.ts +20 -2
- package/src/lib/workspace-apps.ts +4 -1
- package/src/server/lib/app-creation-store.spec.ts +120 -0
- package/src/server/lib/app-creation-store.ts +264 -41
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/dispatch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.1",
|
|
4
4
|
"description": "Dispatch — workspace control plane for agent-native apps. Vault, integrations, destinations, scheduled jobs, and cross-app delegation, shipped as a single drop-in package.",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"tailwind-merge": "^3.5.0",
|
|
88
88
|
"vaul": "^1.1.2",
|
|
89
89
|
"zod": "^4.3.6",
|
|
90
|
-
"@agent-native/toolkit": "0.16.
|
|
90
|
+
"@agent-native/toolkit": "0.16.6"
|
|
91
91
|
},
|
|
92
92
|
"devDependencies": {
|
|
93
93
|
"@react-router/dev": "^8.1.0",
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
"typescript-7": "npm:typescript@^7.0.2",
|
|
102
102
|
"vite": "8.1.0",
|
|
103
103
|
"vitest": "^4.1.5",
|
|
104
|
-
"@agent-native/core": "0.
|
|
104
|
+
"@agent-native/core": "0.164.1"
|
|
105
105
|
},
|
|
106
106
|
"peerDependencies": {
|
|
107
107
|
"@agent-native/core": ">=0.8.0",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
2
3
|
import React, { act } from "react";
|
|
3
4
|
import { createRoot, type Root } from "react-dom/client";
|
|
4
5
|
import { MemoryRouter } from "react-router";
|
|
@@ -99,6 +100,7 @@ vi.mock("./create-app-popover", () => ({
|
|
|
99
100
|
describe("DispatchControlPlane", () => {
|
|
100
101
|
let container: HTMLDivElement;
|
|
101
102
|
let root: Root;
|
|
103
|
+
let queryClient: QueryClient;
|
|
102
104
|
|
|
103
105
|
beforeEach(() => {
|
|
104
106
|
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
@@ -109,6 +111,12 @@ describe("DispatchControlPlane", () => {
|
|
|
109
111
|
clientState.connectedApps = [];
|
|
110
112
|
clientState.curatedTemplates = [];
|
|
111
113
|
clientState.useChatModels.mockClear();
|
|
114
|
+
queryClient = new QueryClient({
|
|
115
|
+
defaultOptions: {
|
|
116
|
+
queries: { retry: false },
|
|
117
|
+
mutations: { retry: false },
|
|
118
|
+
},
|
|
119
|
+
});
|
|
112
120
|
container = document.createElement("div");
|
|
113
121
|
document.body.appendChild(container);
|
|
114
122
|
root = createRoot(container);
|
|
@@ -125,7 +133,9 @@ describe("DispatchControlPlane", () => {
|
|
|
125
133
|
root.render(
|
|
126
134
|
<MemoryRouter initialEntries={["/overview"]}>
|
|
127
135
|
<TooltipProvider>
|
|
128
|
-
<
|
|
136
|
+
<QueryClientProvider client={queryClient}>
|
|
137
|
+
<DispatchControlPlane />
|
|
138
|
+
</QueryClientProvider>
|
|
129
139
|
</TooltipProvider>
|
|
130
140
|
</MemoryRouter>,
|
|
131
141
|
);
|
|
@@ -189,7 +199,9 @@ describe("DispatchControlPlane", () => {
|
|
|
189
199
|
root.render(
|
|
190
200
|
<MemoryRouter initialEntries={["/overview"]}>
|
|
191
201
|
<TooltipProvider>
|
|
192
|
-
<
|
|
202
|
+
<QueryClientProvider client={queryClient}>
|
|
203
|
+
<DispatchControlPlane />
|
|
204
|
+
</QueryClientProvider>
|
|
193
205
|
</TooltipProvider>
|
|
194
206
|
</MemoryRouter>,
|
|
195
207
|
);
|
|
@@ -279,7 +291,9 @@ describe("DispatchControlPlane", () => {
|
|
|
279
291
|
root.render(
|
|
280
292
|
<MemoryRouter initialEntries={["/overview"]}>
|
|
281
293
|
<TooltipProvider>
|
|
282
|
-
<
|
|
294
|
+
<QueryClientProvider client={queryClient}>
|
|
295
|
+
<DispatchControlPlane />
|
|
296
|
+
</QueryClientProvider>
|
|
283
297
|
</TooltipProvider>
|
|
284
298
|
</MemoryRouter>,
|
|
285
299
|
);
|
|
@@ -7,6 +7,15 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
7
7
|
import { TooltipProvider } from "./ui/tooltip";
|
|
8
8
|
import { WorkspaceAppCard } from "./workspace-app-card";
|
|
9
9
|
|
|
10
|
+
vi.mock("@agent-native/core/client/sharing", () => ({
|
|
11
|
+
ShareButton: ({ resourceTitle }: { resourceTitle?: string }) =>
|
|
12
|
+
React.createElement(
|
|
13
|
+
"button",
|
|
14
|
+
{ type: "button", "aria-label": `Share ${resourceTitle ?? "app"}` },
|
|
15
|
+
"Share",
|
|
16
|
+
),
|
|
17
|
+
}));
|
|
18
|
+
|
|
10
19
|
vi.mock("@agent-native/core/client/hooks", () => ({
|
|
11
20
|
useActionMutation: () => ({ mutate: vi.fn(), isPending: false }),
|
|
12
21
|
useActionQuery: () => ({
|
|
@@ -26,11 +35,18 @@ vi.mock("@agent-native/core/client/i18n", () => ({
|
|
|
26
35
|
String(values?.defaultValue ?? key),
|
|
27
36
|
}));
|
|
28
37
|
|
|
38
|
+
const frameState = vi.hoisted(() => ({ inBuilderFrame: false }));
|
|
39
|
+
|
|
40
|
+
vi.mock("@agent-native/core/client/host", () => ({
|
|
41
|
+
isInBuilderFrame: () => frameState.inBuilderFrame,
|
|
42
|
+
}));
|
|
43
|
+
|
|
29
44
|
describe("WorkspaceAppCard", () => {
|
|
30
45
|
let container: HTMLDivElement;
|
|
31
46
|
let root: Root;
|
|
32
47
|
|
|
33
48
|
beforeEach(() => {
|
|
49
|
+
frameState.inBuilderFrame = false;
|
|
34
50
|
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
35
51
|
container = document.createElement("div");
|
|
36
52
|
document.body.appendChild(container);
|
|
@@ -118,6 +134,7 @@ describe("WorkspaceAppCard", () => {
|
|
|
118
134
|
});
|
|
119
135
|
|
|
120
136
|
it("opens mounted workspace apps at their published URL", async () => {
|
|
137
|
+
frameState.inBuilderFrame = true;
|
|
121
138
|
const originalParent = window.parent;
|
|
122
139
|
const originalTop = window.top;
|
|
123
140
|
const topWindow = { location: { href: "" } } as unknown as Window;
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
useActionQuery,
|
|
5
5
|
} from "@agent-native/core/client/hooks";
|
|
6
6
|
import { useT } from "@agent-native/core/client/i18n";
|
|
7
|
+
import { ShareButton } from "@agent-native/core/client/sharing";
|
|
7
8
|
import {
|
|
8
9
|
IconChevronDown,
|
|
9
10
|
IconChevronRight,
|
|
@@ -210,6 +211,17 @@ export function WorkspaceAppCard({
|
|
|
210
211
|
pendingOpenLabel={pendingOpenLabel}
|
|
211
212
|
onTogglePinned={onTogglePinned}
|
|
212
213
|
/>
|
|
214
|
+
{!isPending ? (
|
|
215
|
+
<div className="pointer-events-auto">
|
|
216
|
+
<ShareButton
|
|
217
|
+
resourceType="workspace-app"
|
|
218
|
+
resourceId={app.id}
|
|
219
|
+
resourceTitle={app.name}
|
|
220
|
+
trigger="icon"
|
|
221
|
+
triggerClassName={APP_CARD_ACTION_CLASS}
|
|
222
|
+
/>
|
|
223
|
+
</div>
|
|
224
|
+
) : null}
|
|
213
225
|
<WorkspaceAppSettings
|
|
214
226
|
app={app}
|
|
215
227
|
isArchived={isArchived}
|
|
@@ -382,8 +382,9 @@ describe("WorkspaceAppKeepAlive", () => {
|
|
|
382
382
|
expect(clientState.workspaceSsoMutateAsync).not.toHaveBeenCalled();
|
|
383
383
|
});
|
|
384
384
|
|
|
385
|
-
it("mints custom SSO before navigating
|
|
385
|
+
it("mints custom SSO before navigating a Builder-hosted surface", async () => {
|
|
386
386
|
clientState.workspaceSsoEnabled = true;
|
|
387
|
+
clientState.inBuilderFrame = true;
|
|
387
388
|
const navigateToTopWindow = vi.fn(() => true);
|
|
388
389
|
Object.defineProperty(window, "parent", {
|
|
389
390
|
configurable: true,
|
|
@@ -535,9 +536,8 @@ describe("WorkspaceAppKeepAlive", () => {
|
|
|
535
536
|
).toBe("Workspace SSO is temporarily unavailable");
|
|
536
537
|
});
|
|
537
538
|
|
|
538
|
-
it("
|
|
539
|
+
it("keeps a normal iframe app inline", async () => {
|
|
539
540
|
const topWindow = { location: { href: "" } } as unknown as Window;
|
|
540
|
-
const expectedUrl = new URL("/mail", window.location.href).href;
|
|
541
541
|
Object.defineProperty(window, "parent", {
|
|
542
542
|
configurable: true,
|
|
543
543
|
value: {},
|
|
@@ -552,15 +552,22 @@ describe("WorkspaceAppKeepAlive", () => {
|
|
|
552
552
|
<WorkspaceAppFrame app={{ id: "mail", name: "Mail", path: "/mail" }} />,
|
|
553
553
|
);
|
|
554
554
|
await Promise.resolve();
|
|
555
|
+
await Promise.resolve();
|
|
556
|
+
await Promise.resolve();
|
|
555
557
|
});
|
|
556
558
|
|
|
557
|
-
expect(topWindow.location.href).toBe(
|
|
558
|
-
expect(clientState.legacyMutateAsync).
|
|
559
|
-
|
|
559
|
+
expect(topWindow.location.href).toBe("");
|
|
560
|
+
expect(clientState.legacyMutateAsync).toHaveBeenCalledWith({
|
|
561
|
+
app: "mail",
|
|
562
|
+
path: "/mail",
|
|
563
|
+
chrome: "minimal",
|
|
564
|
+
});
|
|
565
|
+
expect(container.querySelector("iframe")).not.toBeNull();
|
|
560
566
|
});
|
|
561
567
|
|
|
562
568
|
it("falls back to the embedded app when top-window navigation is blocked", async () => {
|
|
563
569
|
const navigateToTopWindow = vi.fn(() => false);
|
|
570
|
+
clientState.inBuilderFrame = true;
|
|
564
571
|
Object.defineProperty(window, "parent", {
|
|
565
572
|
configurable: true,
|
|
566
573
|
value: {},
|
|
@@ -594,6 +601,7 @@ describe("WorkspaceAppKeepAlive", () => {
|
|
|
594
601
|
"/mail/emails?status=failed#latest",
|
|
595
602
|
window.location.href,
|
|
596
603
|
).href;
|
|
604
|
+
clientState.inBuilderFrame = true;
|
|
597
605
|
Object.defineProperty(window, "parent", {
|
|
598
606
|
configurable: true,
|
|
599
607
|
value: {},
|
package/src/db/index.ts
CHANGED
|
@@ -1,9 +1,36 @@
|
|
|
1
|
-
import { createGetDb } from "@agent-native/core/db";
|
|
1
|
+
import { createGetDb, getDbExec } from "@agent-native/core/db";
|
|
2
|
+
import { registerShareableResource } from "@agent-native/core/sharing";
|
|
2
3
|
|
|
3
4
|
import * as schema from "./schema.js";
|
|
4
5
|
|
|
5
6
|
export const getDb = createGetDb(schema);
|
|
6
7
|
|
|
8
|
+
registerShareableResource({
|
|
9
|
+
type: "workspace-app",
|
|
10
|
+
resourceTable: schema.workspaceApps,
|
|
11
|
+
sharesTable: schema.workspaceAppShares,
|
|
12
|
+
displayName: "Workspace app",
|
|
13
|
+
titleColumn: "name",
|
|
14
|
+
getResourcePath: (resource) =>
|
|
15
|
+
typeof resource?.path === "string" ? resource.path : undefined,
|
|
16
|
+
getDb,
|
|
17
|
+
allowPublic: false,
|
|
18
|
+
requireOrgMemberForUserShares: true,
|
|
19
|
+
supportsGroupShares: true,
|
|
20
|
+
canManageAccess: async (resource, ctx) => {
|
|
21
|
+
if (!ctx.userEmail || !ctx.orgId || resource?.orgId !== ctx.orgId) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const result = await getDbExec().execute({
|
|
25
|
+
sql: `SELECT 1 FROM org_members
|
|
26
|
+
WHERE org_id = ? AND LOWER(email) = LOWER(?)
|
|
27
|
+
AND role IN ('owner', 'admin') LIMIT 1`,
|
|
28
|
+
args: [ctx.orgId, ctx.userEmail],
|
|
29
|
+
});
|
|
30
|
+
return result.rows.length > 0;
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
7
34
|
export function db() {
|
|
8
35
|
return getDb();
|
|
9
36
|
}
|
package/src/db/schema.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { table, text, integer } from "@agent-native/core/db/schema";
|
|
2
2
|
|
|
3
|
+
// Workspace app ownership/sharing lives in core so every mounted app can
|
|
4
|
+
// enforce the same access record. Re-exporting these tables keeps Dispatch's
|
|
5
|
+
// Drizzle accessor aware of them for the generic share actions.
|
|
6
|
+
export { workspaceApps, workspaceAppShares } from "@agent-native/core/org";
|
|
7
|
+
|
|
3
8
|
export const dispatchDestinations = table("dispatch_destinations", {
|
|
4
9
|
id: text("id").primaryKey(),
|
|
5
10
|
ownerEmail: text("owner_email").notNull(),
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
navigateToWorkspaceApp,
|
|
6
|
+
shouldOpenWorkspaceAppInTopWindow,
|
|
6
7
|
workspaceAppDirectHref,
|
|
7
8
|
workspaceAppEmbedTarget,
|
|
8
9
|
} from "./workspace-apps";
|
|
@@ -66,7 +67,12 @@ describe("workspaceAppDirectHref", () => {
|
|
|
66
67
|
});
|
|
67
68
|
|
|
68
69
|
describe("navigateToWorkspaceApp", () => {
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
window.history.replaceState({}, "", "/");
|
|
72
|
+
});
|
|
73
|
+
|
|
69
74
|
afterEach(() => {
|
|
75
|
+
window.history.replaceState({}, "", "/");
|
|
70
76
|
Object.defineProperty(window, "parent", {
|
|
71
77
|
configurable: true,
|
|
72
78
|
value: window,
|
|
@@ -77,8 +83,18 @@ describe("navigateToWorkspaceApp", () => {
|
|
|
77
83
|
});
|
|
78
84
|
});
|
|
79
85
|
|
|
80
|
-
it("
|
|
86
|
+
it("keeps a normal iframe inline", () => {
|
|
87
|
+
Object.defineProperty(window, "parent", {
|
|
88
|
+
configurable: true,
|
|
89
|
+
value: {},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(shouldOpenWorkspaceAppInTopWindow()).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("uses the top window when Dispatch is inside a Builder webview", () => {
|
|
81
96
|
const topWindow = { location: { href: "" } } as unknown as Window;
|
|
97
|
+
window.history.replaceState({}, "", "/?builder.preview=interact");
|
|
82
98
|
Object.defineProperty(window, "parent", {
|
|
83
99
|
configurable: true,
|
|
84
100
|
value: {},
|
|
@@ -88,6 +104,7 @@ describe("navigateToWorkspaceApp", () => {
|
|
|
88
104
|
value: topWindow,
|
|
89
105
|
});
|
|
90
106
|
|
|
107
|
+
expect(shouldOpenWorkspaceAppInTopWindow()).toBe(true);
|
|
91
108
|
navigateToWorkspaceApp("/mail");
|
|
92
109
|
|
|
93
110
|
expect(topWindow.location.href).toBe(
|
|
@@ -104,6 +121,7 @@ describe("navigateToWorkspaceApp", () => {
|
|
|
104
121
|
},
|
|
105
122
|
});
|
|
106
123
|
const topWindow = { location } as unknown as Window;
|
|
124
|
+
window.history.replaceState({}, "", "/?builder.preview=interact");
|
|
107
125
|
Object.defineProperty(window, "parent", {
|
|
108
126
|
configurable: true,
|
|
109
127
|
value: {},
|
|
@@ -15,6 +15,7 @@ export interface WorkspaceAppSummary {
|
|
|
15
15
|
url?: string | null;
|
|
16
16
|
isDispatch?: boolean;
|
|
17
17
|
audience?: "internal" | "public";
|
|
18
|
+
visibility?: "private" | "org";
|
|
18
19
|
publicPaths?: string[];
|
|
19
20
|
protectedPaths?: string[];
|
|
20
21
|
status?: "ready" | "pending";
|
|
@@ -255,7 +256,9 @@ export function isPendingBuilderHref(app: WorkspaceAppSummary): boolean {
|
|
|
255
256
|
|
|
256
257
|
export function shouldOpenWorkspaceAppInTopWindow(): boolean {
|
|
257
258
|
if (typeof window === "undefined") return false;
|
|
258
|
-
|
|
259
|
+
// A generic iframe is an inline host by design. Only Builder owns the
|
|
260
|
+
// parent navigation contract for workspace apps.
|
|
261
|
+
return isInBuilderFrame();
|
|
259
262
|
}
|
|
260
263
|
|
|
261
264
|
export function navigateToWorkspaceApp(href: string): boolean {
|
|
@@ -26,6 +26,11 @@ const mocks = vi.hoisted(() => {
|
|
|
26
26
|
putSetting: vi.fn(async (key: string, value: unknown) => {
|
|
27
27
|
settings.set(key, value);
|
|
28
28
|
}),
|
|
29
|
+
getOrgSetting: vi.fn(async () => null),
|
|
30
|
+
resolveAccess: vi.fn(async () => ({
|
|
31
|
+
role: "viewer",
|
|
32
|
+
resource: {},
|
|
33
|
+
})),
|
|
29
34
|
getDbExec: vi.fn(() => ({
|
|
30
35
|
execute: vi.fn(async () => ({
|
|
31
36
|
rows: state.orgRole ? [{ role: state.orgRole }] : [],
|
|
@@ -74,8 +79,18 @@ vi.mock("@agent-native/core/db", async (importOriginal) => {
|
|
|
74
79
|
vi.mock("@agent-native/core/settings", () => ({
|
|
75
80
|
getSetting: (...args: any[]) => mocks.getSetting(...args),
|
|
76
81
|
putSetting: (...args: any[]) => mocks.putSetting(...args),
|
|
82
|
+
getOrgSetting: (...args: any[]) => mocks.getOrgSetting(...args),
|
|
77
83
|
}));
|
|
78
84
|
|
|
85
|
+
vi.mock("@agent-native/core/sharing", async (importOriginal) => {
|
|
86
|
+
const actual =
|
|
87
|
+
await importOriginal<typeof import("@agent-native/core/sharing")>();
|
|
88
|
+
return {
|
|
89
|
+
...actual,
|
|
90
|
+
resolveAccess: (...args: any[]) => mocks.resolveAccess(...args),
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
|
|
79
94
|
vi.mock("@agent-native/core/server", async (importOriginal) => {
|
|
80
95
|
const actual =
|
|
81
96
|
await importOriginal<typeof import("@agent-native/core/server")>();
|
|
@@ -105,6 +120,14 @@ afterEach(() => {
|
|
|
105
120
|
vi.clearAllMocks();
|
|
106
121
|
mocks.settings.clear();
|
|
107
122
|
mocks.state.orgRole = "admin";
|
|
123
|
+
mocks.getDbExec.mockReset();
|
|
124
|
+
mocks.getDbExec.mockImplementation(() => ({
|
|
125
|
+
execute: vi.fn(async () => ({
|
|
126
|
+
rows: mocks.state.orgRole ? [{ role: mocks.state.orgRole }] : [],
|
|
127
|
+
})),
|
|
128
|
+
}));
|
|
129
|
+
mocks.resolveAccess.mockReset();
|
|
130
|
+
mocks.resolveAccess.mockResolvedValue({ role: "viewer", resource: {} });
|
|
108
131
|
mocks.resolveBuilderCredentialsDetailed.mockResolvedValue({
|
|
109
132
|
privateKey: null,
|
|
110
133
|
publicKey: null,
|
|
@@ -345,6 +368,103 @@ describe("listWorkspaceApps", () => {
|
|
|
345
368
|
expect(apps.map((app) => app.id)).toEqual(["dispatch"]);
|
|
346
369
|
});
|
|
347
370
|
|
|
371
|
+
it("does not create or expose apps when the org visibility setting cannot be read", async () => {
|
|
372
|
+
stubManifest([
|
|
373
|
+
{ id: "dispatch", name: "Dispatch", path: "/dispatch" },
|
|
374
|
+
{ id: "private-app", name: "Private app", path: "/private-app" },
|
|
375
|
+
]);
|
|
376
|
+
mocks.getOrgSetting.mockRejectedValueOnce(
|
|
377
|
+
new Error("settings store unavailable"),
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
await expect(
|
|
381
|
+
runWithRequestContext(
|
|
382
|
+
{ userEmail: "dev@example.test", orgId: "org-123" },
|
|
383
|
+
() => listWorkspaceApps({ includeAgentCards: false }),
|
|
384
|
+
),
|
|
385
|
+
).rejects.toThrow("settings store unavailable");
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("fails closed when the workspace-app access schema is unavailable", async () => {
|
|
389
|
+
stubManifest([
|
|
390
|
+
{ id: "dispatch", name: "Dispatch", path: "/dispatch" },
|
|
391
|
+
{ id: "private-app", name: "Private app", path: "/private-app" },
|
|
392
|
+
]);
|
|
393
|
+
mocks.resolveAccess.mockRejectedValueOnce(
|
|
394
|
+
new Error("no such table: workspace_app_shares"),
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
const apps = await runWithRequestContext(
|
|
398
|
+
{ userEmail: "dev@example.test", orgId: "org-123" },
|
|
399
|
+
() => listWorkspaceApps({ includeAgentCards: false }),
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
expect(apps.map((app) => app.id)).toEqual(["dispatch"]);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("does not expose the workspace app registry without an authenticated user", async () => {
|
|
406
|
+
stubNoPendingContext();
|
|
407
|
+
stubManifest([
|
|
408
|
+
{ id: "dispatch", name: "Dispatch", path: "/dispatch" },
|
|
409
|
+
{ id: "private-app", name: "Private app", path: "/private-app" },
|
|
410
|
+
]);
|
|
411
|
+
|
|
412
|
+
const apps = await runWithRequestContext({ orgId: "org-123" }, () =>
|
|
413
|
+
listWorkspaceApps({ includeAgentCards: false }),
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
expect(apps).toEqual([]);
|
|
417
|
+
expect(mocks.resolveAccess).not.toHaveBeenCalled();
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it("uses the organization default only for apps with trusted creation metadata", async () => {
|
|
421
|
+
stubNoPendingContext();
|
|
422
|
+
stubManifest([
|
|
423
|
+
{
|
|
424
|
+
id: "legacy-app",
|
|
425
|
+
name: "Legacy app",
|
|
426
|
+
path: "/legacy-app",
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
id: "new-app",
|
|
430
|
+
name: "New app",
|
|
431
|
+
path: "/new-app",
|
|
432
|
+
createdBy: "creator@example.test",
|
|
433
|
+
},
|
|
434
|
+
]);
|
|
435
|
+
mocks.getOrgSetting.mockResolvedValueOnce({ visibility: "private" });
|
|
436
|
+
const execute = vi.fn(async (statement: unknown) => {
|
|
437
|
+
const sql =
|
|
438
|
+
typeof statement === "string"
|
|
439
|
+
? statement
|
|
440
|
+
: String((statement as { sql?: unknown })?.sql ?? "");
|
|
441
|
+
if (sql.includes("SELECT id, owner_email, org_id, visibility")) {
|
|
442
|
+
return { rows: [], rowsAffected: 0 };
|
|
443
|
+
}
|
|
444
|
+
return { rows: [], rowsAffected: 1 };
|
|
445
|
+
});
|
|
446
|
+
mocks.getDbExec.mockReturnValue({ execute });
|
|
447
|
+
|
|
448
|
+
const apps = await runWithRequestContext(
|
|
449
|
+
{ userEmail: "dev@example.test", orgId: "org-123" },
|
|
450
|
+
() => listWorkspaceApps({ includeAgentCards: false }),
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
expect(apps.find((app) => app.id === "legacy-app")?.visibility).toBe("org");
|
|
454
|
+
expect(apps.find((app) => app.id === "new-app")?.visibility).toBe(
|
|
455
|
+
"private",
|
|
456
|
+
);
|
|
457
|
+
const inserts = execute.mock.calls.filter(([statement]) =>
|
|
458
|
+
String((statement as { sql?: unknown })?.sql ?? "").includes(
|
|
459
|
+
"INSERT INTO workspace_apps",
|
|
460
|
+
),
|
|
461
|
+
);
|
|
462
|
+
expect(inserts.map(([statement]) => (statement as any).args?.[3])).toEqual([
|
|
463
|
+
"org",
|
|
464
|
+
"private",
|
|
465
|
+
]);
|
|
466
|
+
});
|
|
467
|
+
|
|
348
468
|
it("projects exact custom SSO eligibility without exposing registry details", async () => {
|
|
349
469
|
stubNoPendingContext();
|
|
350
470
|
stubManifest([
|