@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.
Files changed (69) hide show
  1. package/agent/AgentDetailView.d.ts +7 -9
  2. package/agent/AgentDetailView.d.ts.map +1 -1
  3. package/agent/AgentDetailView.js +22 -30
  4. package/agent/AgentDetailView.js.map +1 -1
  5. package/index.d.ts +2 -2
  6. package/index.d.ts.map +1 -1
  7. package/index.js +2 -2
  8. package/index.js.map +1 -1
  9. package/package.json +4 -4
  10. package/sharing/AgentShareList.d.ts +44 -0
  11. package/sharing/AgentShareList.d.ts.map +1 -0
  12. package/sharing/AgentShareList.js +149 -0
  13. package/sharing/AgentShareList.js.map +1 -0
  14. package/sharing/ShareAgentDialog.d.ts +38 -25
  15. package/sharing/ShareAgentDialog.d.ts.map +1 -1
  16. package/sharing/ShareAgentDialog.js +131 -62
  17. package/sharing/ShareAgentDialog.js.map +1 -1
  18. package/sharing/index.d.ts +10 -8
  19. package/sharing/index.d.ts.map +1 -1
  20. package/sharing/index.js +5 -4
  21. package/sharing/index.js.map +1 -1
  22. package/sharing/useAgentShares.d.ts +42 -0
  23. package/sharing/useAgentShares.d.ts.map +1 -0
  24. package/sharing/useAgentShares.js +38 -0
  25. package/sharing/useAgentShares.js.map +1 -0
  26. package/sharing/useCanCreateAgentShare.d.ts +43 -0
  27. package/sharing/useCanCreateAgentShare.d.ts.map +1 -0
  28. package/sharing/useCanCreateAgentShare.js +44 -0
  29. package/sharing/useCanCreateAgentShare.js.map +1 -0
  30. package/sharing/useDeleteAgentShare.d.ts +42 -0
  31. package/sharing/useDeleteAgentShare.d.ts.map +1 -0
  32. package/sharing/useDeleteAgentShare.js +45 -0
  33. package/sharing/useDeleteAgentShare.js.map +1 -0
  34. package/sharing/useSaveAgentShare.d.ts +43 -18
  35. package/sharing/useSaveAgentShare.d.ts.map +1 -1
  36. package/sharing/useSaveAgentShare.js +55 -22
  37. package/sharing/useSaveAgentShare.js.map +1 -1
  38. package/src/agent/AgentDetailView.tsx +34 -43
  39. package/src/index.ts +11 -9
  40. package/src/sharing/AgentShareList.tsx +507 -0
  41. package/src/sharing/ShareAgentDialog.tsx +310 -112
  42. package/src/sharing/__tests__/AgentShareList.test.tsx +388 -0
  43. package/src/sharing/__tests__/ShareAgentDialog.test.tsx +311 -312
  44. package/src/sharing/__tests__/useAgentShares.test.tsx +145 -0
  45. package/src/sharing/__tests__/useCanCreateAgentShare.test.tsx +190 -0
  46. package/src/sharing/index.ts +10 -12
  47. package/src/sharing/useAgentShares.ts +68 -0
  48. package/src/sharing/useCanCreateAgentShare.ts +74 -0
  49. package/src/sharing/useDeleteAgentShare.ts +73 -0
  50. package/src/sharing/useSaveAgentShare.ts +73 -23
  51. package/styles.css +1 -1
  52. package/sharing/useAgentShare.d.ts +0 -49
  53. package/sharing/useAgentShare.d.ts.map +0 -1
  54. package/sharing/useAgentShare.js +0 -64
  55. package/sharing/useAgentShare.js.map +0 -1
  56. package/sharing/useCreateExternalShareLink.d.ts +0 -90
  57. package/sharing/useCreateExternalShareLink.d.ts.map +0 -1
  58. package/sharing/useCreateExternalShareLink.js +0 -65
  59. package/sharing/useCreateExternalShareLink.js.map +0 -1
  60. package/sharing/useShareAgent.d.ts +0 -69
  61. package/sharing/useShareAgent.d.ts.map +0 -1
  62. package/sharing/useShareAgent.js +0 -42
  63. package/sharing/useShareAgent.js.map +0 -1
  64. package/src/sharing/__tests__/useAgentShare.test.tsx +0 -210
  65. package/src/sharing/__tests__/useCreateExternalShareLink.test.tsx +0 -194
  66. package/src/sharing/__tests__/useShareAgent.test.tsx +0 -131
  67. package/src/sharing/useAgentShare.ts +0 -105
  68. package/src/sharing/useCreateExternalShareLink.tsx +0 -141
  69. 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
+ });
@@ -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 { useAgentShare } from "./useAgentShare.js";
9
- export type { UseAgentShareReturn } from "./useAgentShare.js";
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 { useShareAgent } from "./useShareAgent.js";
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
+ }