@stigmer/react 3.1.13 → 3.1.14
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/agent/AgentDetailView.d.ts +7 -9
- package/agent/AgentDetailView.d.ts.map +1 -1
- package/agent/AgentDetailView.js +22 -30
- package/agent/AgentDetailView.js.map +1 -1
- package/index.d.ts +2 -2
- package/index.d.ts.map +1 -1
- package/index.js +2 -2
- package/index.js.map +1 -1
- package/package.json +4 -4
- package/sharing/AgentShareList.d.ts +44 -0
- package/sharing/AgentShareList.d.ts.map +1 -0
- package/sharing/AgentShareList.js +149 -0
- package/sharing/AgentShareList.js.map +1 -0
- package/sharing/ShareAgentDialog.d.ts +38 -25
- package/sharing/ShareAgentDialog.d.ts.map +1 -1
- package/sharing/ShareAgentDialog.js +131 -62
- package/sharing/ShareAgentDialog.js.map +1 -1
- package/sharing/index.d.ts +10 -8
- package/sharing/index.d.ts.map +1 -1
- package/sharing/index.js +5 -4
- package/sharing/index.js.map +1 -1
- package/sharing/useAgentShares.d.ts +42 -0
- package/sharing/useAgentShares.d.ts.map +1 -0
- package/sharing/useAgentShares.js +38 -0
- package/sharing/useAgentShares.js.map +1 -0
- package/sharing/useCanCreateAgentShare.d.ts +43 -0
- package/sharing/useCanCreateAgentShare.d.ts.map +1 -0
- package/sharing/useCanCreateAgentShare.js +44 -0
- package/sharing/useCanCreateAgentShare.js.map +1 -0
- package/sharing/useDeleteAgentShare.d.ts +42 -0
- package/sharing/useDeleteAgentShare.d.ts.map +1 -0
- package/sharing/useDeleteAgentShare.js +45 -0
- package/sharing/useDeleteAgentShare.js.map +1 -0
- package/sharing/useSaveAgentShare.d.ts +43 -18
- package/sharing/useSaveAgentShare.d.ts.map +1 -1
- package/sharing/useSaveAgentShare.js +55 -22
- package/sharing/useSaveAgentShare.js.map +1 -1
- package/src/agent/AgentDetailView.tsx +34 -43
- package/src/index.ts +11 -9
- package/src/sharing/AgentShareList.tsx +507 -0
- package/src/sharing/ShareAgentDialog.tsx +310 -112
- package/src/sharing/__tests__/AgentShareList.test.tsx +388 -0
- package/src/sharing/__tests__/ShareAgentDialog.test.tsx +311 -312
- package/src/sharing/__tests__/useAgentShares.test.tsx +145 -0
- package/src/sharing/__tests__/useCanCreateAgentShare.test.tsx +190 -0
- package/src/sharing/index.ts +10 -12
- package/src/sharing/useAgentShares.ts +68 -0
- package/src/sharing/useCanCreateAgentShare.ts +74 -0
- package/src/sharing/useDeleteAgentShare.ts +73 -0
- package/src/sharing/useSaveAgentShare.ts +73 -23
- package/styles.css +1 -1
- package/sharing/useAgentShare.d.ts +0 -49
- package/sharing/useAgentShare.d.ts.map +0 -1
- package/sharing/useAgentShare.js +0 -64
- package/sharing/useAgentShare.js.map +0 -1
- package/sharing/useCreateExternalShareLink.d.ts +0 -90
- package/sharing/useCreateExternalShareLink.d.ts.map +0 -1
- package/sharing/useCreateExternalShareLink.js +0 -65
- package/sharing/useCreateExternalShareLink.js.map +0 -1
- package/sharing/useShareAgent.d.ts +0 -69
- package/sharing/useShareAgent.d.ts.map +0 -1
- package/sharing/useShareAgent.js +0 -42
- package/sharing/useShareAgent.js.map +0 -1
- package/src/sharing/__tests__/useAgentShare.test.tsx +0 -210
- package/src/sharing/__tests__/useCreateExternalShareLink.test.tsx +0 -194
- package/src/sharing/__tests__/useShareAgent.test.tsx +0 -131
- package/src/sharing/useAgentShare.ts +0 -105
- package/src/sharing/useCreateExternalShareLink.tsx +0 -141
- package/src/sharing/useShareAgent.tsx +0 -107
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { renderHook, waitFor } from "@testing-library/react";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import type { GetAgentSharesByAgentRequest } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/io_pb";
|
|
5
|
+
import { StigmerContext } from "../../context";
|
|
6
|
+
import { FetchCacheContext } from "../../internal/FetchCacheProvider";
|
|
7
|
+
import { useAgentShares } from "../useAgentShares";
|
|
8
|
+
|
|
9
|
+
function createMockStigmer(overrides: {
|
|
10
|
+
getByAgent?: (input: GetAgentSharesByAgentRequest) => Promise<unknown>;
|
|
11
|
+
} = {}) {
|
|
12
|
+
return {
|
|
13
|
+
agentShare: {
|
|
14
|
+
getByAgent:
|
|
15
|
+
overrides.getByAgent ??
|
|
16
|
+
vi.fn().mockResolvedValue({ totalCount: 0, items: [] }),
|
|
17
|
+
},
|
|
18
|
+
} as never;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function wrapper(client: unknown) {
|
|
22
|
+
return function Wrapper({ children }: { children: ReactNode }) {
|
|
23
|
+
return (
|
|
24
|
+
<FetchCacheContext.Provider value={null}>
|
|
25
|
+
<StigmerContext.Provider value={client as never}>
|
|
26
|
+
{children}
|
|
27
|
+
</StigmerContext.Provider>
|
|
28
|
+
</FetchCacheContext.Provider>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function makeShare(org: string, slug: string, id = `ash_${org}_${slug}`) {
|
|
34
|
+
return {
|
|
35
|
+
metadata: { id, org, slug },
|
|
36
|
+
spec: { enabled: true },
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("useAgentShares", () => {
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
vi.restoreAllMocks();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("loads the agent's shares by agent id", async () => {
|
|
46
|
+
const share = makeShare("acme", "support-agent");
|
|
47
|
+
const getByAgent = vi
|
|
48
|
+
.fn()
|
|
49
|
+
.mockResolvedValue({ totalCount: 1, items: [share] });
|
|
50
|
+
const client = createMockStigmer({ getByAgent });
|
|
51
|
+
|
|
52
|
+
const { result } = renderHook(() => useAgentShares("agt_1"), {
|
|
53
|
+
wrapper: wrapper(client),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(result.current.isLoading).toBe(true);
|
|
57
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
58
|
+
|
|
59
|
+
expect(result.current.shares).toEqual([share]);
|
|
60
|
+
expect(result.current.error).toBeNull();
|
|
61
|
+
expect(getByAgent).toHaveBeenCalledWith(
|
|
62
|
+
expect.objectContaining({ agentId: "agt_1" }),
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("returns the FULL list — never collapses to a canonical share", async () => {
|
|
67
|
+
// An agent can carry N shares across N orgs (decision 011 D3 +
|
|
68
|
+
// decision 013): the owner's, a renamed sibling, another org's
|
|
69
|
+
// external channel. The management surface shows them all.
|
|
70
|
+
const owner = makeShare("acme", "support-agent");
|
|
71
|
+
const renamed = makeShare("acme", "support-help-desk");
|
|
72
|
+
const external = makeShare("consumer-org", "support-agent");
|
|
73
|
+
const getByAgent = vi
|
|
74
|
+
.fn()
|
|
75
|
+
.mockResolvedValue({ totalCount: 3, items: [owner, renamed, external] });
|
|
76
|
+
const client = createMockStigmer({ getByAgent });
|
|
77
|
+
|
|
78
|
+
const { result } = renderHook(() => useAgentShares("agt_1"), {
|
|
79
|
+
wrapper: wrapper(client),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
83
|
+
expect(result.current.shares).toEqual([owner, renamed, external]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("resolves empty (no shares yet) when the agent has never been shared", async () => {
|
|
87
|
+
const client = createMockStigmer();
|
|
88
|
+
|
|
89
|
+
const { result } = renderHook(() => useAgentShares("agt_1"), {
|
|
90
|
+
wrapper: wrapper(client),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
94
|
+
|
|
95
|
+
// The no-share state is not an error: creating the first share is
|
|
96
|
+
// the empty state's call to action.
|
|
97
|
+
expect(result.current.shares).toEqual([]);
|
|
98
|
+
expect(result.current.error).toBeNull();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("skips fetching while the agent id is empty (stable no-op)", () => {
|
|
102
|
+
const getByAgent = vi.fn();
|
|
103
|
+
const client = createMockStigmer({ getByAgent });
|
|
104
|
+
|
|
105
|
+
const { result } = renderHook(() => useAgentShares(""), {
|
|
106
|
+
wrapper: wrapper(client),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(result.current.isLoading).toBe(false);
|
|
110
|
+
expect(result.current.shares).toEqual([]);
|
|
111
|
+
expect(getByAgent).not.toHaveBeenCalled();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("exposes fetch failures as errors", async () => {
|
|
115
|
+
const getByAgent = vi
|
|
116
|
+
.fn()
|
|
117
|
+
.mockRejectedValue(new Error("backend unavailable"));
|
|
118
|
+
const client = createMockStigmer({ getByAgent });
|
|
119
|
+
|
|
120
|
+
const { result } = renderHook(() => useAgentShares("agt_1"), {
|
|
121
|
+
wrapper: wrapper(client),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
125
|
+
expect(result.current.error).toBeTruthy();
|
|
126
|
+
expect(result.current.shares).toEqual([]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("refetches on demand", async () => {
|
|
130
|
+
const getByAgent = vi
|
|
131
|
+
.fn()
|
|
132
|
+
.mockResolvedValue({ totalCount: 0, items: [] });
|
|
133
|
+
const client = createMockStigmer({ getByAgent });
|
|
134
|
+
|
|
135
|
+
const { result } = renderHook(() => useAgentShares("agt_1"), {
|
|
136
|
+
wrapper: wrapper(client),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
140
|
+
expect(getByAgent).toHaveBeenCalledTimes(1);
|
|
141
|
+
|
|
142
|
+
result.current.refetch();
|
|
143
|
+
await waitFor(() => expect(getByAgent).toHaveBeenCalledTimes(2));
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { renderHook, waitFor } from "@testing-library/react";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
5
|
+
import { StigmerContext } from "../../context";
|
|
6
|
+
import { FetchCacheContext } from "../../internal/FetchCacheProvider";
|
|
7
|
+
import { useCanCreateAgentShare } from "../useCanCreateAgentShare";
|
|
8
|
+
|
|
9
|
+
function createMockStigmer(overrides: {
|
|
10
|
+
isAuthorized?: boolean;
|
|
11
|
+
checkMyPermission?: (...args: unknown[]) => Promise<unknown>;
|
|
12
|
+
} = {}) {
|
|
13
|
+
return {
|
|
14
|
+
iamPolicy: {
|
|
15
|
+
checkMyPermission:
|
|
16
|
+
overrides.checkMyPermission ??
|
|
17
|
+
vi.fn().mockResolvedValue({
|
|
18
|
+
isAuthorized: overrides.isAuthorized ?? true,
|
|
19
|
+
}),
|
|
20
|
+
},
|
|
21
|
+
} as never;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function wrapper(client: unknown) {
|
|
25
|
+
return function Wrapper({ children }: { children: ReactNode }) {
|
|
26
|
+
return (
|
|
27
|
+
<FetchCacheContext.Provider value={null}>
|
|
28
|
+
<StigmerContext.Provider value={client as never}>
|
|
29
|
+
{children}
|
|
30
|
+
</StigmerContext.Provider>
|
|
31
|
+
</FetchCacheContext.Provider>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function makeAgent(visibility?: ApiResourceVisibility) {
|
|
37
|
+
return {
|
|
38
|
+
metadata: {
|
|
39
|
+
id: "agt_1",
|
|
40
|
+
org: "acme",
|
|
41
|
+
slug: "support-agent",
|
|
42
|
+
name: "Support Agent",
|
|
43
|
+
...(visibility !== undefined && { visibility }),
|
|
44
|
+
},
|
|
45
|
+
spec: {},
|
|
46
|
+
} as never;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function permissionCheckOf(client: unknown) {
|
|
50
|
+
return (
|
|
51
|
+
client as { iamPolicy: { checkMyPermission: ReturnType<typeof vi.fn> } }
|
|
52
|
+
).iamPolicy.checkMyPermission;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe("useCanCreateAgentShare", () => {
|
|
56
|
+
it("is not allowed while the agent is loading — no affordance flash", () => {
|
|
57
|
+
const client = createMockStigmer();
|
|
58
|
+
const { result } = renderHook(() => useCanCreateAgentShare(null, "acme"), {
|
|
59
|
+
wrapper: wrapper(client),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(result.current.allowed).toBe(false);
|
|
63
|
+
expect(permissionCheckOf(client)).not.toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("same-org (viewer org equals the agent's, or omitted)", () => {
|
|
67
|
+
it("requires agent can_edit — the server's Phase A create bar", async () => {
|
|
68
|
+
const client = createMockStigmer({ isAuthorized: true });
|
|
69
|
+
const { result } = renderHook(
|
|
70
|
+
() => useCanCreateAgentShare(makeAgent(), "acme"),
|
|
71
|
+
{ wrapper: wrapper(client) },
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
await waitFor(() => expect(result.current.allowed).toBe(true));
|
|
75
|
+
expect(result.current.isCrossOrg).toBe(false);
|
|
76
|
+
expect(result.current.shareOrg).toBe("acme");
|
|
77
|
+
|
|
78
|
+
const input = permissionCheckOf(client).mock.calls[0][0] as {
|
|
79
|
+
resource?: { kind: string; id: string };
|
|
80
|
+
relation: string;
|
|
81
|
+
};
|
|
82
|
+
expect(input.relation).toBe("can_edit");
|
|
83
|
+
expect(input.resource?.kind).toBe("agent");
|
|
84
|
+
expect(input.resource?.id).toBe("agt_1");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("defaults the share org to the agent's own when viewerOrg is omitted", async () => {
|
|
88
|
+
const client = createMockStigmer({ isAuthorized: true });
|
|
89
|
+
const { result } = renderHook(
|
|
90
|
+
() => useCanCreateAgentShare(makeAgent()),
|
|
91
|
+
{ wrapper: wrapper(client) },
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
await waitFor(() => expect(result.current.allowed).toBe(true));
|
|
95
|
+
expect(result.current.shareOrg).toBe("acme");
|
|
96
|
+
expect(result.current.isCrossOrg).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("refuses when the viewer lacks can_edit", async () => {
|
|
100
|
+
const client = createMockStigmer({ isAuthorized: false });
|
|
101
|
+
const { result } = renderHook(
|
|
102
|
+
() => useCanCreateAgentShare(makeAgent(), "acme"),
|
|
103
|
+
{ wrapper: wrapper(client) },
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
await waitFor(() => expect(permissionCheckOf(client)).toHaveBeenCalled());
|
|
107
|
+
await waitFor(() => expect(result.current.allowed).toBe(false));
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("cross-org (decision 013 D2's two-sided bar)", () => {
|
|
112
|
+
it("allows on a public agent when the viewer holds can_create_agent_share in their org", async () => {
|
|
113
|
+
const client = createMockStigmer({ isAuthorized: true });
|
|
114
|
+
const { result } = renderHook(
|
|
115
|
+
() =>
|
|
116
|
+
useCanCreateAgentShare(
|
|
117
|
+
makeAgent(ApiResourceVisibility.visibility_public),
|
|
118
|
+
"consumer-org",
|
|
119
|
+
),
|
|
120
|
+
{ wrapper: wrapper(client) },
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
await waitFor(() => expect(result.current.allowed).toBe(true));
|
|
124
|
+
expect(result.current.isCrossOrg).toBe(true);
|
|
125
|
+
expect(result.current.shareOrg).toBe("consumer-org");
|
|
126
|
+
|
|
127
|
+
const input = permissionCheckOf(client).mock.calls[0][0] as {
|
|
128
|
+
resource?: { kind: string; id: string };
|
|
129
|
+
relation: string;
|
|
130
|
+
};
|
|
131
|
+
expect(input.relation).toBe("can_create_agent_share");
|
|
132
|
+
expect(input.resource?.kind).toBe("organization");
|
|
133
|
+
// An Organization's id equals its slug (ApiResourceMetadata.id).
|
|
134
|
+
expect(input.resource?.id).toBe("consumer-org");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("refuses on a non-public agent without any permission RPC — visibility IS the consent (D1)", () => {
|
|
138
|
+
const client = createMockStigmer();
|
|
139
|
+
const { result } = renderHook(
|
|
140
|
+
() => useCanCreateAgentShare(makeAgent(), "consumer-org"),
|
|
141
|
+
{ wrapper: wrapper(client) },
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
expect(result.current.allowed).toBe(false);
|
|
145
|
+
expect(result.current.isCrossOrg).toBe(true);
|
|
146
|
+
expect(permissionCheckOf(client)).not.toHaveBeenCalled();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("refuses when the viewer lacks can_create_agent_share", async () => {
|
|
150
|
+
const client = createMockStigmer({ isAuthorized: false });
|
|
151
|
+
const { result } = renderHook(
|
|
152
|
+
() =>
|
|
153
|
+
useCanCreateAgentShare(
|
|
154
|
+
makeAgent(ApiResourceVisibility.visibility_public),
|
|
155
|
+
"consumer-org",
|
|
156
|
+
),
|
|
157
|
+
{ wrapper: wrapper(client) },
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
await waitFor(() => expect(permissionCheckOf(client)).toHaveBeenCalled());
|
|
161
|
+
await waitFor(() => expect(result.current.allowed).toBe(false));
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("creates in the VIEWER's org even when the viewer could also edit the agent", async () => {
|
|
165
|
+
// The case that produced the old double-entry confusion: a user
|
|
166
|
+
// holding can_edit on a public agent while acting as another org.
|
|
167
|
+
// There is exactly one answer now — the share lands in the org
|
|
168
|
+
// they are acting as, on that org's bill.
|
|
169
|
+
const client = createMockStigmer({ isAuthorized: true });
|
|
170
|
+
const { result } = renderHook(
|
|
171
|
+
() =>
|
|
172
|
+
useCanCreateAgentShare(
|
|
173
|
+
makeAgent(ApiResourceVisibility.visibility_public),
|
|
174
|
+
"personal",
|
|
175
|
+
),
|
|
176
|
+
{ wrapper: wrapper(client) },
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
await waitFor(() => expect(result.current.allowed).toBe(true));
|
|
180
|
+
expect(result.current.shareOrg).toBe("personal");
|
|
181
|
+
expect(result.current.isCrossOrg).toBe(true);
|
|
182
|
+
// Only the org-side bar is consulted — the agent-side can_edit
|
|
183
|
+
// check belongs to the same-org branch alone.
|
|
184
|
+
const relations = permissionCheckOf(client).mock.calls.map(
|
|
185
|
+
(call: unknown[]) => (call[0] as { relation: string }).relation,
|
|
186
|
+
);
|
|
187
|
+
expect(relations).toEqual(["can_create_agent_share"]);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
});
|
package/src/sharing/index.ts
CHANGED
|
@@ -5,31 +5,29 @@ export type {
|
|
|
5
5
|
} from "./useSharedAgentProfile.js";
|
|
6
6
|
export { SharedAgentChat } from "./SharedAgentChat.js";
|
|
7
7
|
export type { SharedAgentChatProps } from "./SharedAgentChat.js";
|
|
8
|
-
export {
|
|
9
|
-
export type {
|
|
8
|
+
export { useAgentShares } from "./useAgentShares.js";
|
|
9
|
+
export type { UseAgentSharesReturn } from "./useAgentShares.js";
|
|
10
|
+
export { useCanCreateAgentShare } from "./useCanCreateAgentShare.js";
|
|
11
|
+
export type { UseCanCreateAgentShareReturn } from "./useCanCreateAgentShare.js";
|
|
10
12
|
export {
|
|
13
|
+
draftFromShare,
|
|
11
14
|
sharingAudienceFromProto,
|
|
12
15
|
useSaveAgentShare,
|
|
13
16
|
} from "./useSaveAgentShare.js";
|
|
14
17
|
export type {
|
|
18
|
+
AgentShareCreateIdentity,
|
|
15
19
|
AgentShareDraft,
|
|
16
20
|
SharingAudience,
|
|
17
21
|
UseSaveAgentShareReturn,
|
|
18
22
|
} from "./useSaveAgentShare.js";
|
|
23
|
+
export { useDeleteAgentShare } from "./useDeleteAgentShare.js";
|
|
24
|
+
export type { UseDeleteAgentShareReturn } from "./useDeleteAgentShare.js";
|
|
19
25
|
export { useRotateShareLink } from "./useRotateShareLink.js";
|
|
20
26
|
export type { UseRotateShareLinkReturn } from "./useRotateShareLink.js";
|
|
21
27
|
export { ShareAgentDialog } from "./ShareAgentDialog.js";
|
|
22
28
|
export type { ShareAgentDialogProps } from "./ShareAgentDialog.js";
|
|
23
|
-
export {
|
|
24
|
-
export type {
|
|
25
|
-
UseShareAgentArgs,
|
|
26
|
-
UseShareAgentReturn,
|
|
27
|
-
} from "./useShareAgent.js";
|
|
28
|
-
export { useCreateExternalShareLink } from "./useCreateExternalShareLink.js";
|
|
29
|
-
export type {
|
|
30
|
-
UseCreateExternalShareLinkArgs,
|
|
31
|
-
UseCreateExternalShareLinkReturn,
|
|
32
|
-
} from "./useCreateExternalShareLink.js";
|
|
29
|
+
export { AgentShareList } from "./AgentShareList.js";
|
|
30
|
+
export type { AgentShareListProps } from "./AgentShareList.js";
|
|
33
31
|
export { useShareToolReadiness } from "./useShareToolReadiness.js";
|
|
34
32
|
export type { ShareToolReadiness } from "./useShareToolReadiness.js";
|
|
35
33
|
// Origin validation moved to @stigmer/sdk (framework-free, shared with the
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { create } from "@bufbuild/protobuf";
|
|
4
|
+
import type { AgentShare } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/api_pb";
|
|
5
|
+
import { GetAgentSharesByAgentRequestSchema } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/io_pb";
|
|
6
|
+
import { useStigmer } from "../hooks.js";
|
|
7
|
+
import { useFetch } from "../internal/useFetch.js";
|
|
8
|
+
|
|
9
|
+
/** Return value of {@link useAgentShares}. */
|
|
10
|
+
export interface UseAgentSharesReturn {
|
|
11
|
+
/**
|
|
12
|
+
* Every share of the agent visible to the caller, in server order.
|
|
13
|
+
* Empty while loading, on error, or when the agent has never been
|
|
14
|
+
* shared — `shares.length === 0 && !isLoading && !error` means "no
|
|
15
|
+
* shares exist yet".
|
|
16
|
+
*/
|
|
17
|
+
readonly shares: readonly AgentShare[];
|
|
18
|
+
/** `true` while the initial fetch or a refetch is in flight. */
|
|
19
|
+
readonly isLoading: boolean;
|
|
20
|
+
/** `true` while a background refetch is in flight and stale data is shown. */
|
|
21
|
+
readonly isRefetching: boolean;
|
|
22
|
+
/** Error from the last failed request, or `null` when healthy. */
|
|
23
|
+
readonly error: Error | null;
|
|
24
|
+
/** Discard cached data and re-fetch the shares from the server. */
|
|
25
|
+
readonly refetch: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Data hook that loads all of an agent's {@link AgentShare} resources —
|
|
30
|
+
* the channels carrying its hosted-chat configuration (audience, allowed
|
|
31
|
+
* origins, visitor messages, tool credentials, link token).
|
|
32
|
+
*
|
|
33
|
+
* Sharing is channel configuration, not agent behavior (decision 011):
|
|
34
|
+
* an agent can carry N shares, each with its own URL, billing org, and
|
|
35
|
+
* credentials (decision 011 D3 + decision 013 cross-org shares). This
|
|
36
|
+
* hook returns the full list; the server already scopes it to shares the
|
|
37
|
+
* caller can view (FGA-filtered in cloud, unfiltered in the single-user
|
|
38
|
+
* OSS edition), so a viewer sees their own org's channels and never
|
|
39
|
+
* another org's.
|
|
40
|
+
*
|
|
41
|
+
* Pass an empty `agentId` to skip fetching (stable no-op) — useful
|
|
42
|
+
* while the agent is still loading.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```tsx
|
|
46
|
+
* const { shares, isLoading, refetch } = useAgentShares(agent?.metadata?.id ?? "");
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export function useAgentShares(agentId: string): UseAgentSharesReturn {
|
|
50
|
+
const stigmer = useStigmer();
|
|
51
|
+
|
|
52
|
+
const fetchFn = agentId
|
|
53
|
+
? async () => {
|
|
54
|
+
const result = await stigmer.agentShare.getByAgent(
|
|
55
|
+
create(GetAgentSharesByAgentRequestSchema, { agentId }),
|
|
56
|
+
);
|
|
57
|
+
return result.items;
|
|
58
|
+
}
|
|
59
|
+
: null;
|
|
60
|
+
|
|
61
|
+
const { data: shares, isLoading, isRefetching, error, refetch } = useFetch(
|
|
62
|
+
fetchFn,
|
|
63
|
+
[agentId, stigmer],
|
|
64
|
+
[] as AgentShare[],
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return { shares, isLoading, isRefetching, error, refetch };
|
|
68
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
4
|
+
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
5
|
+
import { useCheckPermission } from "../iam-policy/useCheckPermission.js";
|
|
6
|
+
|
|
7
|
+
/** Return value of {@link useCanCreateAgentShare}. */
|
|
8
|
+
export interface UseCanCreateAgentShareReturn {
|
|
9
|
+
/** Whether the viewer may create a share of this agent in `shareOrg`. */
|
|
10
|
+
readonly allowed: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* The org that would own the new share — the viewer's org when set,
|
|
13
|
+
* else the agent's own. The share's URL, billing, and credential
|
|
14
|
+
* bindings all belong to this org.
|
|
15
|
+
*/
|
|
16
|
+
readonly shareOrg: string;
|
|
17
|
+
/** Whether creating in `shareOrg` would be a cross-org share (decision 013). */
|
|
18
|
+
readonly isCrossOrg: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Mirrors the server's AgentShare create bar so create affordances never
|
|
23
|
+
* appear to a user whose create would be refused:
|
|
24
|
+
*
|
|
25
|
+
* - **Same-org** (`shareOrg` equals the agent's org): agent `can_edit` —
|
|
26
|
+
* the exact permission the create/apply handlers enforce on the
|
|
27
|
+
* referenced agent.
|
|
28
|
+
* - **Cross-org** (decision 013 D2, two-sided): the agent must be
|
|
29
|
+
* `visibility_public` — the origin org's implicit consent (D1), and
|
|
30
|
+
* the client-side proxy for the `can_execute`-via-`allow_public` half
|
|
31
|
+
* of the bar — and the viewer must hold `can_create_agent_share` in
|
|
32
|
+
* their own org (admin-level: a public share spends that org's credits
|
|
33
|
+
* on the open internet). The org check uses the org slug as the
|
|
34
|
+
* resource id — an Organization's id equals its slug
|
|
35
|
+
* (ApiResourceMetadata.id).
|
|
36
|
+
*
|
|
37
|
+
* On the OSS edition {@link useCheckPermission} degrades to allowed
|
|
38
|
+
* (no IAM service), matching the backend's documented no-op
|
|
39
|
+
* authorization (decision 011 D4).
|
|
40
|
+
*
|
|
41
|
+
* Pass `null` for `agent` while it loads — `allowed` stays `false` so
|
|
42
|
+
* no affordance flashes before the gate can be evaluated.
|
|
43
|
+
*
|
|
44
|
+
* @param agent The agent to share, or `null` while loading.
|
|
45
|
+
* @param viewerOrg The viewer's active org. Empty/omitted means the
|
|
46
|
+
* agent's own org (the same-org owner flow).
|
|
47
|
+
*/
|
|
48
|
+
export function useCanCreateAgentShare(
|
|
49
|
+
agent: Agent | null,
|
|
50
|
+
viewerOrg?: string,
|
|
51
|
+
): UseCanCreateAgentShareReturn {
|
|
52
|
+
const agentId = agent?.metadata?.id ?? "";
|
|
53
|
+
const agentOrg = agent?.metadata?.org ?? "";
|
|
54
|
+
const shareOrg = viewerOrg || agentOrg;
|
|
55
|
+
const isCrossOrg = shareOrg !== "" && shareOrg !== agentOrg;
|
|
56
|
+
const isPublic =
|
|
57
|
+
agent?.metadata?.visibility === ApiResourceVisibility.visibility_public;
|
|
58
|
+
|
|
59
|
+
// Both permission checks are declared unconditionally (hook rules);
|
|
60
|
+
// each skips its RPC (null resource) when its branch doesn't apply.
|
|
61
|
+
const { allowed: canEditAgent } = useCheckPermission(
|
|
62
|
+
!isCrossOrg && agentId ? { kind: "agent", id: agentId } : null,
|
|
63
|
+
"can_edit",
|
|
64
|
+
);
|
|
65
|
+
const { allowed: canCreateInOrg } = useCheckPermission(
|
|
66
|
+
isCrossOrg && isPublic ? { kind: "organization", id: shareOrg } : null,
|
|
67
|
+
"can_create_agent_share",
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const allowed =
|
|
71
|
+
!!agent && (isCrossOrg ? isPublic && canCreateInOrg : canEditAgent);
|
|
72
|
+
|
|
73
|
+
return { allowed, shareOrg, isCrossOrg };
|
|
74
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState } from "react";
|
|
4
|
+
import type { AgentShare } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/api_pb";
|
|
5
|
+
import { useStigmer } from "../hooks.js";
|
|
6
|
+
import { toError } from "../internal/toError.js";
|
|
7
|
+
|
|
8
|
+
/** Return value of {@link useDeleteAgentShare}. */
|
|
9
|
+
export interface UseDeleteAgentShareReturn {
|
|
10
|
+
/**
|
|
11
|
+
* Delete an agent share by ID. Returns the deleted resource.
|
|
12
|
+
*
|
|
13
|
+
* Deleting a share is full teardown (decision 011 D1): its hosted
|
|
14
|
+
* link dies immediately — including for visitors mid-conversation —
|
|
15
|
+
* and its configuration (origins, messages, credential bindings, link
|
|
16
|
+
* token) is gone. To stop serving while keeping the configuration,
|
|
17
|
+
* save the share with `enabled: false` instead (a config-preserving
|
|
18
|
+
* pause via {@link useSaveAgentShare}).
|
|
19
|
+
*/
|
|
20
|
+
readonly deleteShare: (id: string) => Promise<AgentShare>;
|
|
21
|
+
/** `true` while the delete request is in flight. */
|
|
22
|
+
readonly isDeleting: boolean;
|
|
23
|
+
/** Error from the last failed delete, or `null` when healthy. */
|
|
24
|
+
readonly error: Error | null;
|
|
25
|
+
/** Reset `error` to `null`. */
|
|
26
|
+
readonly clearError: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Mutation hook that deletes an {@link AgentShare} resource.
|
|
31
|
+
*
|
|
32
|
+
* Wraps `stigmer.agentShare.delete()` (authorized by the share's
|
|
33
|
+
* `can_delete`) with loading/error state. The caller is responsible for
|
|
34
|
+
* post-delete UI updates (e.g. refreshing the share list) and for
|
|
35
|
+
* confirming the destructive action — the delete-vs-pause distinction
|
|
36
|
+
* belongs in the confirmation copy.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```tsx
|
|
40
|
+
* const { deleteShare, isDeleting } = useDeleteAgentShare();
|
|
41
|
+
*
|
|
42
|
+
* const handleDelete = async () => {
|
|
43
|
+
* await deleteShare(share.metadata.id);
|
|
44
|
+
* refetch(); // refresh the share list
|
|
45
|
+
* };
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export function useDeleteAgentShare(): UseDeleteAgentShareReturn {
|
|
49
|
+
const stigmer = useStigmer();
|
|
50
|
+
const [isDeleting, setIsDeleting] = useState(false);
|
|
51
|
+
const [error, setError] = useState<Error | null>(null);
|
|
52
|
+
|
|
53
|
+
const clearError = useCallback(() => setError(null), []);
|
|
54
|
+
|
|
55
|
+
const deleteShare = useCallback(
|
|
56
|
+
async (id: string): Promise<AgentShare> => {
|
|
57
|
+
setIsDeleting(true);
|
|
58
|
+
setError(null);
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
return await stigmer.agentShare.delete(id);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
setError(toError(err));
|
|
64
|
+
throw err;
|
|
65
|
+
} finally {
|
|
66
|
+
setIsDeleting(false);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
[stigmer],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return { deleteShare, isDeleting, error, clearError };
|
|
73
|
+
}
|