@stigmer/react 3.40.0 → 3.41.0

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 (279) hide show
  1. package/README.md +1 -0
  2. package/access/ManageAccessDialog.d.ts +1 -1
  3. package/access/ManageAccessDialog.js +1 -1
  4. package/access/types.d.ts +1 -1
  5. package/billing/ChangePlanDialog.d.ts.map +1 -1
  6. package/billing/ChangePlanDialog.js +13 -1
  7. package/billing/ChangePlanDialog.js.map +1 -1
  8. package/billing/ProviderKeysSection.d.ts +33 -0
  9. package/billing/ProviderKeysSection.d.ts.map +1 -0
  10. package/billing/ProviderKeysSection.js +143 -0
  11. package/billing/ProviderKeysSection.js.map +1 -0
  12. package/billing/UpgradeNotice.d.ts.map +1 -1
  13. package/billing/UpgradeNotice.js +1 -0
  14. package/billing/UpgradeNotice.js.map +1 -1
  15. package/billing/index.d.ts +4 -0
  16. package/billing/index.d.ts.map +1 -1
  17. package/billing/index.js +2 -0
  18. package/billing/index.js.map +1 -1
  19. package/billing/plan-features.d.ts +3 -2
  20. package/billing/plan-features.d.ts.map +1 -1
  21. package/billing/plan-features.js +4 -3
  22. package/billing/plan-features.js.map +1 -1
  23. package/billing/useProviderKeys.d.ts +67 -0
  24. package/billing/useProviderKeys.d.ts.map +1 -0
  25. package/billing/useProviderKeys.js +61 -0
  26. package/billing/useProviderKeys.js.map +1 -0
  27. package/channel/AgentChannelsPanel.d.ts.map +1 -1
  28. package/channel/AgentChannelsPanel.js +11 -9
  29. package/channel/AgentChannelsPanel.js.map +1 -1
  30. package/channel/connect/ServingAppSection.js +1 -1
  31. package/channel/connect/ServingAppSection.js.map +1 -1
  32. package/channel-app/ChannelAppDetailPanel.d.ts +5 -0
  33. package/channel-app/ChannelAppDetailPanel.d.ts.map +1 -1
  34. package/channel-app/ChannelAppDetailPanel.js +22 -9
  35. package/channel-app/ChannelAppDetailPanel.js.map +1 -1
  36. package/channel-app/ChannelAppListPanel.d.ts +13 -4
  37. package/channel-app/ChannelAppListPanel.d.ts.map +1 -1
  38. package/channel-app/ChannelAppListPanel.js +5 -5
  39. package/channel-app/ChannelAppListPanel.js.map +1 -1
  40. package/channel-app/CreateChannelAppForm.js +1 -1
  41. package/channel-app/CreateChannelAppForm.js.map +1 -1
  42. package/channel-app/copy.d.ts +15 -0
  43. package/channel-app/copy.d.ts.map +1 -0
  44. package/channel-app/copy.js +15 -0
  45. package/channel-app/copy.js.map +1 -0
  46. package/channel-app/internal.d.ts +9 -0
  47. package/channel-app/internal.d.ts.map +1 -1
  48. package/channel-app/internal.js +9 -0
  49. package/channel-app/internal.js.map +1 -1
  50. package/channel-app/slackAppSetup.d.ts +3 -1
  51. package/channel-app/slackAppSetup.d.ts.map +1 -1
  52. package/channel-app/slackAppSetup.js +3 -1
  53. package/channel-app/slackAppSetup.js.map +1 -1
  54. package/channel-app/whatsappAppSetup.d.ts +3 -1
  55. package/channel-app/whatsappAppSetup.d.ts.map +1 -1
  56. package/channel-app/whatsappAppSetup.js +3 -1
  57. package/channel-app/whatsappAppSetup.js.map +1 -1
  58. package/composer/SessionComposer.d.ts.map +1 -1
  59. package/composer/SessionComposer.js +5 -9
  60. package/composer/SessionComposer.js.map +1 -1
  61. package/environment/EnvironmentVariableEditor.js +1 -1
  62. package/environment/EnvironmentVariableEditor.js.map +1 -1
  63. package/environment/index.d.ts +1 -1
  64. package/environment/index.d.ts.map +1 -1
  65. package/environment/index.js +1 -1
  66. package/environment/index.js.map +1 -1
  67. package/environment/systemEnvVars.d.ts +4 -63
  68. package/environment/systemEnvVars.d.ts.map +1 -1
  69. package/environment/systemEnvVars.js +21 -113
  70. package/environment/systemEnvVars.js.map +1 -1
  71. package/execution/RecalledMemoriesCard.js +1 -1
  72. package/iam-policy/OrgMembersPanel.js +1 -1
  73. package/iam-policy/OrgMembersPanel.js.map +1 -1
  74. package/iam-policy/useCheckPermission.d.ts +4 -1
  75. package/iam-policy/useCheckPermission.d.ts.map +1 -1
  76. package/iam-policy/useCheckPermission.js +4 -1
  77. package/iam-policy/useCheckPermission.js.map +1 -1
  78. package/iam-policy/useGrantableRoles.d.ts +1 -1
  79. package/iam-policy/useGrantableRoles.js +1 -1
  80. package/iam-policy/useRevokeOrgAccess.d.ts +10 -8
  81. package/iam-policy/useRevokeOrgAccess.d.ts.map +1 -1
  82. package/iam-policy/useRevokeOrgAccess.js +4 -4
  83. package/iam-policy/useRevokeOrgAccess.js.map +1 -1
  84. package/identity-account/AccountPreferencesPanel.d.ts +6 -0
  85. package/identity-account/AccountPreferencesPanel.d.ts.map +1 -1
  86. package/identity-account/AccountPreferencesPanel.js +33 -2
  87. package/identity-account/AccountPreferencesPanel.js.map +1 -1
  88. package/identity-account/useAccountExecutionDefaults.d.ts +1 -1
  89. package/identity-account/useAccountExecutionDefaults.d.ts.map +1 -1
  90. package/identity-account/useAccountExecutionDefaults.js +2 -3
  91. package/identity-account/useAccountExecutionDefaults.js.map +1 -1
  92. package/index.d.ts +3 -3
  93. package/index.d.ts.map +1 -1
  94. package/index.js +3 -3
  95. package/index.js.map +1 -1
  96. package/internal/UnknownPublicAddressNotice.d.ts +20 -0
  97. package/internal/UnknownPublicAddressNotice.d.ts.map +1 -0
  98. package/internal/UnknownPublicAddressNotice.js +19 -0
  99. package/internal/UnknownPublicAddressNotice.js.map +1 -0
  100. package/internal/features.js +2 -2
  101. package/internal/features.js.map +1 -1
  102. package/library/InstanceVisibilitySelector.d.ts +1 -1
  103. package/library/InstanceVisibilitySelector.d.ts.map +1 -1
  104. package/library/ResourceVisibilityControl.d.ts +6 -3
  105. package/library/ResourceVisibilityControl.d.ts.map +1 -1
  106. package/library/ResourceVisibilityControl.js +7 -4
  107. package/library/ResourceVisibilityControl.js.map +1 -1
  108. package/library/VisibilitySelector.d.ts +2 -2
  109. package/library/VisibilitySelector.d.ts.map +1 -1
  110. package/library/VisibilitySelector.js +1 -1
  111. package/licenses/LicenseIssueForm.js +1 -1
  112. package/licenses/LicenseIssueForm.js.map +1 -1
  113. package/licenses/license-format.d.ts +7 -5
  114. package/licenses/license-format.d.ts.map +1 -1
  115. package/licenses/license-format.js +1 -3
  116. package/licenses/license-format.js.map +1 -1
  117. package/mcp-server/McpServerConnectDialog.js +4 -5
  118. package/mcp-server/McpServerConnectDialog.js.map +1 -1
  119. package/mcp-server/McpServerDetailView.d.ts.map +1 -1
  120. package/mcp-server/McpServerDetailView.js +3 -6
  121. package/mcp-server/McpServerDetailView.js.map +1 -1
  122. package/mcp-server/McpServerPicker.d.ts.map +1 -1
  123. package/mcp-server/McpServerPicker.js +2 -6
  124. package/mcp-server/McpServerPicker.js.map +1 -1
  125. package/mcp-server/McpToolSelector.js +1 -1
  126. package/mcp-server/McpToolSelector.js.map +1 -1
  127. package/mcp-server/useMcpServerConnect.d.ts +14 -25
  128. package/mcp-server/useMcpServerConnect.d.ts.map +1 -1
  129. package/mcp-server/useMcpServerConnect.js +7 -19
  130. package/mcp-server/useMcpServerConnect.js.map +1 -1
  131. package/mcp-server/useMcpServerOAuthConnect.d.ts +1 -3
  132. package/mcp-server/useMcpServerOAuthConnect.d.ts.map +1 -1
  133. package/mcp-server/useMcpServerOAuthConnect.js +6 -19
  134. package/mcp-server/useMcpServerOAuthConnect.js.map +1 -1
  135. package/models/HarnessSelector.d.ts.map +1 -1
  136. package/models/HarnessSelector.js +1 -2
  137. package/models/HarnessSelector.js.map +1 -1
  138. package/models/ModelSelector.d.ts +1 -1
  139. package/models/ModelSelector.d.ts.map +1 -1
  140. package/models/ModelSelector.js +1 -7
  141. package/models/ModelSelector.js.map +1 -1
  142. package/models/harness.d.ts +10 -6
  143. package/models/harness.d.ts.map +1 -1
  144. package/models/harness.js +35 -28
  145. package/models/harness.js.map +1 -1
  146. package/models/index.d.ts +1 -1
  147. package/models/index.d.ts.map +1 -1
  148. package/models/index.js +1 -1
  149. package/models/index.js.map +1 -1
  150. package/organization/OrgPreferencesPanel.d.ts.map +1 -1
  151. package/organization/OrgPreferencesPanel.js +13 -1
  152. package/organization/OrgPreferencesPanel.js.map +1 -1
  153. package/package.json +5 -5
  154. package/plugin/useMcpServerReadiness.js +2 -2
  155. package/plugin/useMcpServerReadiness.js.map +1 -1
  156. package/provider-standing/ProviderStandingConsole.js +1 -1
  157. package/provider.d.ts +27 -1
  158. package/provider.d.ts.map +1 -1
  159. package/provider.js +3 -2
  160. package/provider.js.map +1 -1
  161. package/public-base-url-context.d.ts +35 -0
  162. package/public-base-url-context.d.ts.map +1 -0
  163. package/public-base-url-context.js +62 -0
  164. package/public-base-url-context.js.map +1 -0
  165. package/resource-workbench/components/SelectionCheckbox.js +1 -1
  166. package/resource-workbench/components/SelectionCheckbox.js.map +1 -1
  167. package/session/useLocalSessionWorker.d.ts +19 -3
  168. package/session/useLocalSessionWorker.d.ts.map +1 -1
  169. package/session/useLocalSessionWorker.js +37 -4
  170. package/session/useLocalSessionWorker.js.map +1 -1
  171. package/settings/ChannelAppsSection.d.ts +7 -0
  172. package/settings/ChannelAppsSection.d.ts.map +1 -1
  173. package/settings/ChannelAppsSection.js +15 -2
  174. package/settings/ChannelAppsSection.js.map +1 -1
  175. package/settings/ProviderKeysSection.d.ts +3 -0
  176. package/settings/ProviderKeysSection.d.ts.map +1 -0
  177. package/settings/ProviderKeysSection.js +3 -0
  178. package/settings/ProviderKeysSection.js.map +1 -0
  179. package/settings/index.d.ts +2 -0
  180. package/settings/index.d.ts.map +1 -1
  181. package/settings/index.js +1 -0
  182. package/settings/index.js.map +1 -1
  183. package/settings/settings-nav.d.ts.map +1 -1
  184. package/settings/settings-nav.js +2 -1
  185. package/settings/settings-nav.js.map +1 -1
  186. package/sharing/ShareAgentDialog.d.ts.map +1 -1
  187. package/sharing/ShareAgentDialog.js +12 -8
  188. package/sharing/ShareAgentDialog.js.map +1 -1
  189. package/sharing/useCanCreateAgentShare.d.ts +3 -1
  190. package/sharing/useCanCreateAgentShare.d.ts.map +1 -1
  191. package/sharing/useCanCreateAgentShare.js +5 -3
  192. package/sharing/useCanCreateAgentShare.js.map +1 -1
  193. package/sharing/useSaveAgentShare.d.ts +1 -1
  194. package/sharing/useSaveAgentShare.js +1 -1
  195. package/src/access/ManageAccessDialog.tsx +1 -1
  196. package/src/access/types.ts +1 -1
  197. package/src/billing/ChangePlanDialog.tsx +14 -2
  198. package/src/billing/ProviderKeysSection.tsx +324 -0
  199. package/src/billing/UpgradeNotice.tsx +1 -0
  200. package/src/billing/index.ts +9 -0
  201. package/src/billing/plan-features.ts +4 -3
  202. package/src/billing/useProviderKeys.ts +129 -0
  203. package/src/channel/AgentChannelsPanel.tsx +11 -9
  204. package/src/channel/connect/ServingAppSection.tsx +1 -1
  205. package/src/channel-app/ChannelAppDetailPanel.tsx +214 -176
  206. package/src/channel-app/ChannelAppListPanel.tsx +14 -4
  207. package/src/channel-app/CreateChannelAppForm.tsx +0 -1
  208. package/src/channel-app/copy.ts +16 -0
  209. package/src/channel-app/internal.tsx +21 -0
  210. package/src/channel-app/slackAppSetup.ts +3 -1
  211. package/src/channel-app/whatsappAppSetup.ts +3 -1
  212. package/src/composer/SessionComposer.tsx +5 -14
  213. package/src/environment/EnvironmentVariableEditor.tsx +1 -1
  214. package/src/environment/index.ts +1 -7
  215. package/src/environment/systemEnvVars.ts +21 -133
  216. package/src/execution/RecalledMemoriesCard.tsx +1 -1
  217. package/src/iam-policy/OrgMembersPanel.tsx +3 -1
  218. package/src/iam-policy/useCheckPermission.ts +4 -1
  219. package/src/iam-policy/useGrantableRoles.ts +1 -1
  220. package/src/iam-policy/useRevokeOrgAccess.ts +10 -8
  221. package/src/identity-account/AccountPreferencesPanel.tsx +56 -9
  222. package/src/identity-account/useAccountExecutionDefaults.ts +2 -5
  223. package/src/index.ts +9 -5
  224. package/src/internal/UnknownPublicAddressNotice.tsx +31 -0
  225. package/src/internal/features.ts +2 -2
  226. package/src/library/InstanceVisibilitySelector.tsx +1 -1
  227. package/src/library/ResourceVisibilityControl.tsx +7 -4
  228. package/src/library/VisibilitySelector.tsx +2 -2
  229. package/src/licenses/LicenseIssueForm.tsx +1 -1
  230. package/src/licenses/license-format.ts +11 -7
  231. package/src/mcp-server/McpServerConnectDialog.tsx +4 -6
  232. package/src/mcp-server/McpServerDetailView.tsx +3 -6
  233. package/src/mcp-server/McpServerPicker.tsx +2 -12
  234. package/src/mcp-server/McpToolSelector.tsx +1 -1
  235. package/src/mcp-server/useMcpServerConnect.ts +14 -33
  236. package/src/mcp-server/useMcpServerOAuthConnect.ts +7 -23
  237. package/src/models/HarnessSelector.tsx +1 -3
  238. package/src/models/ModelSelector.tsx +2 -7
  239. package/src/models/harness.ts +41 -39
  240. package/src/models/index.ts +1 -1
  241. package/src/organization/OrgPreferencesPanel.tsx +19 -1
  242. package/src/plugin/useMcpServerReadiness.ts +2 -2
  243. package/src/provider-standing/ProviderStandingConsole.tsx +1 -1
  244. package/src/provider.tsx +56 -26
  245. package/src/public-base-url-context.ts +70 -0
  246. package/src/resource-workbench/components/SelectionCheckbox.tsx +1 -1
  247. package/src/session/useLocalSessionWorker.ts +35 -4
  248. package/src/settings/ChannelAppsSection.tsx +19 -2
  249. package/src/settings/ProviderKeysSection.tsx +4 -0
  250. package/src/settings/index.ts +2 -0
  251. package/src/settings/settings-nav.ts +2 -0
  252. package/src/sharing/ShareAgentDialog.tsx +30 -14
  253. package/src/sharing/useCanCreateAgentShare.ts +6 -4
  254. package/src/sharing/useSaveAgentShare.ts +1 -1
  255. package/src/styles.css +14 -4
  256. package/src/workflow/WorkflowTaskApprovalSummary.tsx +14 -7
  257. package/src/workflow/inspector/tabs/BranchesTab.tsx +2 -2
  258. package/src/workflow/inspector/tabs/CatchTab.tsx +1 -1
  259. package/src/workflow/inspector/tabs/IterationTab.tsx +1 -1
  260. package/src/workflow/instance/WorkflowInstanceDetailPanel.tsx +7 -6
  261. package/src/workflow/task-detail/task-approval.ts +20 -13
  262. package/styles.css +1 -1
  263. package/workflow/WorkflowTaskApprovalSummary.d.ts +7 -4
  264. package/workflow/WorkflowTaskApprovalSummary.d.ts.map +1 -1
  265. package/workflow/WorkflowTaskApprovalSummary.js +8 -3
  266. package/workflow/WorkflowTaskApprovalSummary.js.map +1 -1
  267. package/workflow/inspector/tabs/BranchesTab.js +1 -1
  268. package/workflow/inspector/tabs/BranchesTab.js.map +1 -1
  269. package/workflow/inspector/tabs/CatchTab.js +1 -1
  270. package/workflow/inspector/tabs/CatchTab.js.map +1 -1
  271. package/workflow/inspector/tabs/IterationTab.js +1 -1
  272. package/workflow/inspector/tabs/IterationTab.js.map +1 -1
  273. package/workflow/instance/WorkflowInstanceDetailPanel.d.ts.map +1 -1
  274. package/workflow/instance/WorkflowInstanceDetailPanel.js +6 -5
  275. package/workflow/instance/WorkflowInstanceDetailPanel.js.map +1 -1
  276. package/workflow/task-detail/task-approval.d.ts +13 -11
  277. package/workflow/task-detail/task-approval.d.ts.map +1 -1
  278. package/workflow/task-detail/task-approval.js +8 -4
  279. package/workflow/task-detail/task-approval.js.map +1 -1
@@ -0,0 +1,324 @@
1
+ "use client";
2
+
3
+ import { useCallback, useId, useState } from "react";
4
+ import { cn } from "@stigmer/theme";
5
+ import { getUserMessage } from "@stigmer/sdk";
6
+ import { timestampDate } from "@bufbuild/protobuf/wkt";
7
+ import type { ProviderKey } from "@stigmer/protos/ai/stigmer/billing/providerkey/v1/api_pb";
8
+ import { Feature } from "@stigmer/protos/ai/stigmer/platform/v1/entitlement_pb";
9
+ import { Button } from "../button/index.js";
10
+ import { ApiResourceKind, useDeploymentMode, useResourceAvailable } from "../deployment-mode.js";
11
+ import { useCheckPermission } from "../iam-policy/useCheckPermission.js";
12
+ import { CloudFeatureNotice } from "../internal/CloudFeatureNotice.js";
13
+ import { UNSTYLED_LIST } from "../internal/element-resets.js";
14
+ import { useOrg } from "../organization/OrgProvider.js";
15
+ import { lowestPlanWith } from "./plan-features.js";
16
+ import { planUpgradeFeature, UpgradeNotice } from "./UpgradeNotice.js";
17
+ import { useEntitlements } from "./useEntitlements.js";
18
+ import { usePlans } from "./usePlans.js";
19
+ import { useProviderKeyActions, useProviderKeys, type ProviderKeyProvider } from "./useProviderKeys.js";
20
+
21
+ /** The providers, in the order the section lists them. */
22
+ const PROVIDERS: ReadonlyArray<{ readonly id: ProviderKeyProvider; readonly name: string; readonly placeholder: string }> = [
23
+ { id: "anthropic", name: "Anthropic", placeholder: "sk-ant-…" },
24
+ { id: "openai", name: "OpenAI", placeholder: "sk-…" },
25
+ ];
26
+
27
+ /** Props for {@link ProviderKeysSection}. */
28
+ export interface ProviderKeysSectionProps {
29
+ /** Where the plans are, in the host's routing. Defaults to the billing settings route. */
30
+ readonly billingHref?: string;
31
+ /** Additional CSS class names. */
32
+ readonly className?: string;
33
+ }
34
+
35
+ /**
36
+ * Settings section for an organization's own LLM provider keys.
37
+ *
38
+ * On a plan that includes bring-your-own provider keys (Business and
39
+ * above), an admin saves an Anthropic or OpenAI key, and the organization's
40
+ * agents and workflows then run on it: the provider bills the organization
41
+ * directly, and those tokens carry no Stigmer commission and do not count
42
+ * toward the plan's usage share. A saved key is never shown again, only its
43
+ * last four characters. A managed organization is served by its
44
+ * integrator's key for a provider it holds none of, shown read-only.
45
+ *
46
+ * A key the plan no longer allows is kept and listed as not in use, and
47
+ * can be removed; the organization's calls run on Stigmer's keys again,
48
+ * billed as usage. Every member sees the list; admins
49
+ * (`can_manage_billing`) save, replace and remove.
50
+ *
51
+ * Cloud-only: in local mode the runner already uses the developer's own
52
+ * keys directly.
53
+ *
54
+ * @example
55
+ * ```tsx
56
+ * <ProviderKeysSection />
57
+ * ```
58
+ */
59
+ export function ProviderKeysSection({ billingHref, className }: ProviderKeysSectionProps) {
60
+ const headingId = useId();
61
+ const { activeOrg } = useOrg();
62
+ const mode = useDeploymentMode();
63
+ const available = useResourceAvailable(ApiResourceKind.subscription);
64
+ const orgId = activeOrg?.metadata?.id ?? "";
65
+
66
+ return (
67
+ <section aria-labelledby={headingId} className={className}>
68
+ <h2 id={headingId} className="stg:text-foreground stg:mb-1 stg:text-sm stg:font-semibold">
69
+ Provider keys
70
+ </h2>
71
+ <p className="stg:text-muted-foreground stg:mb-4 stg:text-xs">
72
+ Run your agents on your organization&apos;s own Anthropic and OpenAI keys. The provider bills you directly;
73
+ those tokens carry no Stigmer commission and do not count toward your plan&apos;s usage share.
74
+ </p>
75
+ {mode === "local" || !available ? (
76
+ <CloudFeatureNotice>
77
+ Your own provider keys are a Stigmer Cloud plan feature. Local mode already calls the providers with your
78
+ own keys directly.
79
+ </CloudFeatureNotice>
80
+ ) : !orgId ? (
81
+ <p className="stg:text-muted-foreground stg:py-4 stg:text-center stg:text-xs">
82
+ Select an organization to manage its provider keys.
83
+ </p>
84
+ ) : (
85
+ <ProviderKeysContent orgId={orgId} billingHref={billingHref} />
86
+ )}
87
+ </section>
88
+ );
89
+ }
90
+
91
+ function ProviderKeysContent({ orgId, billingHref }: { readonly orgId: string; readonly billingHref?: string }) {
92
+ const listed = useProviderKeys(orgId);
93
+ const entitlements = useEntitlements(orgId);
94
+ const plans = usePlans();
95
+ const manage = useCheckPermission({ kind: "organization", id: orgId }, "can_manage_billing");
96
+ const canManage = !manage.isLoading && manage.allowed;
97
+ const actions = useProviderKeyActions();
98
+ const [editing, setEditing] = useState<ProviderKeyProvider | null>(null);
99
+
100
+ const allowed = entitlements.allows(Feature.byo_provider_keys);
101
+ const unlockingPlanName = lowestPlanWith(plans.plans ?? [], Feature.byo_provider_keys)?.metadata?.name;
102
+ const keys = listed.keys ?? [];
103
+ const refused = planUpgradeFeature(actions.error);
104
+
105
+ const refresh = useCallback(() => {
106
+ listed.refetch();
107
+ entitlements.refetch();
108
+ }, [listed, entitlements]);
109
+
110
+ if (listed.error) {
111
+ return <p className="stg:text-destructive stg:text-xs">{getUserMessage(listed.error)}</p>;
112
+ }
113
+ if (listed.keys === null || allowed === null) {
114
+ return <p className="stg:text-muted-foreground stg:text-xs">Loading provider keys…</p>;
115
+ }
116
+
117
+ return (
118
+ <div className="stg:space-y-3">
119
+ {!allowed && (
120
+ <UpgradeNotice feature={Feature.byo_provider_keys} unlockingPlanName={unlockingPlanName} billingHref={billingHref} />
121
+ )}
122
+ {!allowed && keys.length > 0 && (
123
+ <p className="stg:text-muted-foreground stg:text-xs">
124
+ Your saved keys are kept but not used on this plan: your agents run on Stigmer&apos;s keys, billed as usage.
125
+ You can remove them at any time.
126
+ </p>
127
+ )}
128
+ <ul
129
+ aria-label="Provider keys"
130
+ className={cn(UNSTYLED_LIST, "stg:divide-y stg:divide-border stg:rounded-md stg:border stg:border-border")}
131
+ >
132
+ {PROVIDERS.map((provider) => (
133
+ <ProviderRow
134
+ key={provider.id}
135
+ orgId={orgId}
136
+ provider={provider}
137
+ stored={keys.find((key) => key.provider === provider.id)}
138
+ canSave={canManage && allowed}
139
+ canRemove={canManage}
140
+ editing={editing === provider.id}
141
+ onEdit={() => {
142
+ actions.clearError();
143
+ setEditing(provider.id);
144
+ }}
145
+ onDone={() => {
146
+ setEditing(null);
147
+ refresh();
148
+ }}
149
+ onCancel={() => setEditing(null)}
150
+ actions={actions}
151
+ />
152
+ ))}
153
+ </ul>
154
+ {refused !== null ? (
155
+ <UpgradeNotice feature={refused} error={actions.error} billingHref={billingHref} />
156
+ ) : actions.error ? (
157
+ <p role="alert" className="stg:text-destructive stg:text-xs">
158
+ {getUserMessage(actions.error)}
159
+ </p>
160
+ ) : null}
161
+ <p className="stg:text-muted-foreground stg:text-[0.65rem]">
162
+ A failure on your own key, such as a rejected key or an exhausted provider balance, is reported as your key&apos;s;
163
+ calls are never retried on Stigmer&apos;s keys.
164
+ </p>
165
+ </div>
166
+ );
167
+ }
168
+
169
+ function ProviderRow({
170
+ orgId,
171
+ provider,
172
+ stored,
173
+ canSave,
174
+ canRemove,
175
+ editing,
176
+ onEdit,
177
+ onDone,
178
+ onCancel,
179
+ actions,
180
+ }: {
181
+ readonly orgId: string;
182
+ readonly provider: (typeof PROVIDERS)[number];
183
+ readonly stored: ProviderKey | undefined;
184
+ readonly canSave: boolean;
185
+ readonly canRemove: boolean;
186
+ readonly editing: boolean;
187
+ readonly onEdit: () => void;
188
+ readonly onDone: () => void;
189
+ readonly onCancel: () => void;
190
+ readonly actions: ReturnType<typeof useProviderKeyActions>;
191
+ }) {
192
+ const inputId = useId();
193
+ const [value, setValue] = useState("");
194
+ const [confirming, setConfirming] = useState(false);
195
+ const inherited = stored !== undefined && stored.inheritedFromOrgId !== "";
196
+ const own = stored !== undefined && !inherited;
197
+
198
+ const save = async () => {
199
+ try {
200
+ await actions.setKey(orgId, provider.id, value.trim());
201
+ setValue("");
202
+ onDone();
203
+ } catch {
204
+ // The hook holds the error; the section renders it.
205
+ }
206
+ };
207
+ const remove = async () => {
208
+ try {
209
+ await actions.removeKey(orgId, provider.id);
210
+ setConfirming(false);
211
+ onDone();
212
+ } catch {
213
+ // The hook holds the error; the section renders it.
214
+ }
215
+ };
216
+
217
+ return (
218
+ <li className="stg:space-y-2 stg:px-3 stg:py-2.5">
219
+ <div className="stg:flex stg:items-start stg:justify-between stg:gap-3">
220
+ <div className="stg:min-w-0">
221
+ <p className="stg:text-foreground stg:text-xs stg:font-medium">{provider.name}</p>
222
+ <p className="stg:text-muted-foreground stg:text-[0.7rem]">{describe(stored)}</p>
223
+ </div>
224
+ {!editing && !confirming && (
225
+ <div className="stg:flex stg:shrink-0 stg:gap-2">
226
+ {canSave && !inherited && (
227
+ <Button size="sm" variant="outline" onClick={onEdit} disabled={actions.isSubmitting}>
228
+ {own ? "Replace" : "Add key"}
229
+ </Button>
230
+ )}
231
+ {canRemove && own && (
232
+ <Button
233
+ size="sm"
234
+ variant="ghost"
235
+ onClick={() => {
236
+ actions.clearError();
237
+ setConfirming(true);
238
+ }}
239
+ disabled={actions.isSubmitting}
240
+ >
241
+ Remove
242
+ </Button>
243
+ )}
244
+ </div>
245
+ )}
246
+ </div>
247
+ {confirming && (
248
+ <div className="stg:flex stg:flex-wrap stg:items-center stg:justify-between stg:gap-2 stg:rounded-md stg:border stg:border-destructive/30 stg:bg-destructive-subtle stg:px-2.5 stg:py-2">
249
+ <p className="stg:text-foreground stg:min-w-0 stg:flex-1 stg:text-xs">
250
+ {stored?.inUse
251
+ ? `Remove your ${provider.name} key? Your agents' ${provider.name} calls go back to Stigmer's keys, billed as usage.`
252
+ : `Remove your ${provider.name} key? It is not in use on this plan.`}
253
+ </p>
254
+ <div className="stg:flex stg:shrink-0 stg:gap-2">
255
+ <Button size="sm" variant="destructive" onClick={() => void remove()} disabled={actions.isSubmitting}>
256
+ {actions.isSubmitting ? "Removing…" : "Remove key"}
257
+ </Button>
258
+ <Button size="sm" variant="ghost" onClick={() => setConfirming(false)} disabled={actions.isSubmitting}>
259
+ Cancel
260
+ </Button>
261
+ </div>
262
+ </div>
263
+ )}
264
+ {editing && (
265
+ <form
266
+ className="stg:flex stg:flex-wrap stg:items-end stg:gap-2"
267
+ onSubmit={(event) => {
268
+ event.preventDefault();
269
+ void save();
270
+ }}
271
+ >
272
+ <div className="stg:min-w-48 stg:flex-1 stg:space-y-1">
273
+ <label htmlFor={inputId} className="stg:text-foreground stg:text-xs stg:font-medium">
274
+ {provider.name} API key
275
+ </label>
276
+ <input
277
+ id={inputId}
278
+ type="password"
279
+ autoComplete="off"
280
+ value={value}
281
+ onChange={(event) => setValue(event.target.value)}
282
+ placeholder={provider.placeholder}
283
+ disabled={actions.isSubmitting}
284
+ className={cn(
285
+ "stg:w-full stg:rounded-md stg:border stg:border-input stg:bg-background stg:px-2.5 stg:py-1.5 stg:text-xs stg:text-foreground",
286
+ "stg:placeholder:text-muted-foreground",
287
+ "stg:focus-visible:outline-none stg:focus-visible:ring-1 stg:focus-visible:ring-ring",
288
+ "stg:disabled:pointer-events-none stg:disabled:opacity-50",
289
+ )}
290
+ />
291
+ </div>
292
+ <Button size="sm" type="submit" disabled={actions.isSubmitting || value.trim() === ""}>
293
+ {actions.isSubmitting ? "Saving…" : "Save key"}
294
+ </Button>
295
+ <Button size="sm" variant="ghost" type="button" onClick={onCancel} disabled={actions.isSubmitting}>
296
+ Cancel
297
+ </Button>
298
+ </form>
299
+ )}
300
+ </li>
301
+ );
302
+ }
303
+
304
+ /** One line about a provider's key: none, own, or inherited, with its use. */
305
+ function describe(key: ProviderKey | undefined): string {
306
+ if (key === undefined) {
307
+ return "Not set: calls run on Stigmer's keys, billed as usage.";
308
+ }
309
+ const parts = [`Key ending in ${key.keyHint}`];
310
+ if (key.inheritedFromOrgId !== "") {
311
+ parts.push(`provided by ${key.inheritedFromOrgId}`);
312
+ } else if (key.updatedAt !== undefined) {
313
+ parts.push(`saved ${formatDay(timestampDate(key.updatedAt))}`);
314
+ }
315
+ parts.push(key.lastUsedAt === undefined ? "not used yet" : `last used ${formatDay(timestampDate(key.lastUsedAt))}`);
316
+ if (!key.inUse) {
317
+ parts.push("not in use on this plan");
318
+ }
319
+ return parts.join(" · ");
320
+ }
321
+
322
+ function formatDay(date: Date): string {
323
+ return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", timeZone: "UTC" });
324
+ }
@@ -18,6 +18,7 @@ export const DEFAULT_BILLING_HREF = "/settings/billing";
18
18
  const FEATURE_BY_WIRE_NAME: Readonly<Record<string, Feature>> = {
19
19
  teams: Feature.teams,
20
20
  managed_organizations: Feature.managed_organizations,
21
+ byo_provider_keys: Feature.byo_provider_keys,
21
22
  };
22
23
 
23
24
  /**
@@ -20,6 +20,13 @@ export { useEntitlements } from "./useEntitlements.js";
20
20
  export type { UseEntitlementsOptions, UseEntitlementsReturn } from "./useEntitlements.js";
21
21
  export { usePeriodEstimate } from "./usePeriodEstimate.js";
22
22
  export type { UsePeriodEstimateOptions, UsePeriodEstimateReturn } from "./usePeriodEstimate.js";
23
+ export { useProviderKeys, useProviderKeyActions } from "./useProviderKeys.js";
24
+ export type {
25
+ ProviderKeyProvider,
26
+ UseProviderKeysOptions,
27
+ UseProviderKeysReturn,
28
+ UseProviderKeyActionsReturn,
29
+ } from "./useProviderKeys.js";
23
30
 
24
31
  // Behavior hooks
25
32
  export { useCreateCheckoutSession } from "./useCreateCheckoutSession.js";
@@ -43,6 +50,8 @@ export type {
43
50
  } from "./useSetAutoRechargeConfig.js";
44
51
 
45
52
  // Styled components
53
+ export { ProviderKeysSection } from "./ProviderKeysSection.js";
54
+ export type { ProviderKeysSectionProps } from "./ProviderKeysSection.js";
46
55
  export { BillingSection } from "./BillingSection.js";
47
56
  export type { BillingSectionProps } from "./BillingSection.js";
48
57
  export { CreditBalanceCard } from "./CreditBalanceCard.js";
@@ -24,10 +24,11 @@ const ZERO = BigInt(0);
24
24
  /**
25
25
  * Features a plan may list ahead of the gate that serves them, which
26
26
  * Stigmer Cloud does not offer yet. The console leaves them out of every
27
- * comparison. Bring-your-own provider keys leaves this set when the proxy
28
- * uses an organization's own key.
27
+ * comparison. Empty while every listed feature is served; bring-your-own
28
+ * provider keys left it when the proxy began serving an organization's own
29
+ * key (stigmer/stigmer#1425).
29
30
  */
30
- export const NOT_YET_OFFERED: ReadonlySet<Feature> = new Set([Feature.byo_provider_keys]);
31
+ export const NOT_YET_OFFERED: ReadonlySet<Feature> = new Set<Feature>();
31
32
 
32
33
  /** One feature a plan offers today, with its words. */
33
34
  export interface OfferedFeature {
@@ -0,0 +1,129 @@
1
+ "use client";
2
+
3
+ import { useCallback, useMemo, useState } from "react";
4
+ import { create } from "@bufbuild/protobuf";
5
+ import type { ProviderKey } from "@stigmer/protos/ai/stigmer/billing/providerkey/v1/api_pb";
6
+ import {
7
+ DeleteProviderKeyInputSchema,
8
+ ListProviderKeysInputSchema,
9
+ SetProviderKeyInputSchema,
10
+ } from "@stigmer/protos/ai/stigmer/billing/providerkey/v1/io_pb";
11
+ import { useStigmer } from "../hooks.js";
12
+ import { toError } from "../internal/toError.js";
13
+ import { useFetch } from "../internal/useFetch.js";
14
+
15
+ /** The providers an organization may bring its own key for, by their wire names. */
16
+ export type ProviderKeyProvider = "anthropic" | "openai";
17
+
18
+ /** Options for {@link useProviderKeys}. */
19
+ export interface UseProviderKeysOptions {
20
+ /** Skip fetching while `false` (stable no-op). Default `true`. */
21
+ readonly enabled?: boolean;
22
+ }
23
+
24
+ /** Return value of {@link useProviderKeys}. */
25
+ export interface UseProviderKeysReturn {
26
+ /**
27
+ * The keys that serve the organization, its own and any it inherits from
28
+ * its integrator, without their secrets; `null` before the first
29
+ * successful fetch.
30
+ */
31
+ readonly keys: readonly ProviderKey[] | null;
32
+ /** `true` while the initial fetch is in flight. */
33
+ readonly isLoading: boolean;
34
+ /** Error from the last failed request, or `null` when healthy. */
35
+ readonly error: Error | null;
36
+ /** Discard cached data and re-fetch from the server. */
37
+ readonly refetch: () => void;
38
+ }
39
+
40
+ /**
41
+ * Data hook that lists an organization's own LLM provider keys.
42
+ *
43
+ * No answer carries a key: each row names the provider, the key's last
44
+ * four characters, who saved it and when, when it last served a call, and
45
+ * whether the organization's plan still lets it be used. A managed
46
+ * organization's list includes its integrator's keys for the providers it
47
+ * holds none of (`inheritedFromOrgId`). Every member may read it
48
+ * (`can_view_billing`).
49
+ *
50
+ * Cloud-only.
51
+ *
52
+ * @example
53
+ * ```tsx
54
+ * const { keys } = useProviderKeys(orgId);
55
+ * ```
56
+ */
57
+ export function useProviderKeys(orgId: string | null, options?: UseProviderKeysOptions): UseProviderKeysReturn {
58
+ const stigmer = useStigmer();
59
+ const enabled = (options?.enabled ?? true) && orgId !== null && orgId !== "";
60
+ const { data, isLoading, error, refetch } = useFetch(
61
+ enabled && orgId ? () => stigmer.providerkey.list(create(ListProviderKeysInputSchema, { orgId })) : null,
62
+ [enabled, orgId, stigmer],
63
+ null as { readonly keys: readonly ProviderKey[] } | null,
64
+ );
65
+ const keys = data?.keys ?? null;
66
+ return useMemo(() => ({ keys, isLoading, error, refetch }), [keys, isLoading, error, refetch]);
67
+ }
68
+
69
+ /** Return value of {@link useProviderKeyActions}. */
70
+ export interface UseProviderKeyActionsReturn {
71
+ /**
72
+ * Save the organization's key for a provider, replacing any it holds.
73
+ * The provider is asked about the key once first, and a key it rejects is
74
+ * refused. Refused with the `PLAN_UPGRADE_REQUIRED` reason when the
75
+ * organization's plan does not include bring-your-own provider keys.
76
+ * Resolves with the saved key, without its secret.
77
+ */
78
+ readonly setKey: (orgId: string, provider: ProviderKeyProvider, apiKey: string) => Promise<ProviderKey>;
79
+ /** Remove the organization's key for a provider; allowed on every plan. */
80
+ readonly removeKey: (orgId: string, provider: ProviderKeyProvider) => Promise<ProviderKey>;
81
+ /** `true` while a save or a removal is in flight. */
82
+ readonly isSubmitting: boolean;
83
+ /** Error from the last failed attempt, or `null` when healthy. */
84
+ readonly error: Error | null;
85
+ /** Reset `error` to `null`. */
86
+ readonly clearError: () => void;
87
+ }
88
+
89
+ /**
90
+ * Behaviour hook that saves and removes an organization's own provider
91
+ * keys. The caller needs `can_manage_billing` on the organization (its
92
+ * admins). Refetch {@link useProviderKeys} after a change.
93
+ */
94
+ export function useProviderKeyActions(): UseProviderKeyActionsReturn {
95
+ const stigmer = useStigmer();
96
+ const [isSubmitting, setIsSubmitting] = useState(false);
97
+ const [error, setError] = useState<Error | null>(null);
98
+
99
+ const run = useCallback(async (call: () => Promise<ProviderKey>): Promise<ProviderKey> => {
100
+ setIsSubmitting(true);
101
+ setError(null);
102
+ try {
103
+ return await call();
104
+ } catch (e) {
105
+ const err = toError(e);
106
+ setError(err);
107
+ throw err;
108
+ } finally {
109
+ setIsSubmitting(false);
110
+ }
111
+ }, []);
112
+
113
+ const setKey = useCallback(
114
+ (orgId: string, provider: ProviderKeyProvider, apiKey: string) =>
115
+ run(() => stigmer.providerkey.set(create(SetProviderKeyInputSchema, { orgId, provider, apiKey }))),
116
+ [run, stigmer.providerkey],
117
+ );
118
+ const removeKey = useCallback(
119
+ (orgId: string, provider: ProviderKeyProvider) =>
120
+ run(() => stigmer.providerkey.delete(create(DeleteProviderKeyInputSchema, { orgId, provider }))),
121
+ [run, stigmer.providerkey],
122
+ );
123
+ const clearError = useCallback(() => setError(null), []);
124
+
125
+ return useMemo(
126
+ () => ({ setKey, removeKey, isSubmitting, error, clearError }),
127
+ [setKey, removeKey, isSubmitting, error, clearError],
128
+ );
129
+ }
@@ -119,19 +119,21 @@ export function AgentChannelsPanel({
119
119
  // label worth showing.
120
120
  const { channelApps } = useChannelAppList(agent.metadata?.org || null);
121
121
 
122
- // Mirrors the server's create bar (agent can_edit — the permission the
123
- // create/apply handlers enforce on the referenced agent) so the connect
124
- // affordance never appears to a viewer whose create would be refused.
122
+ // Mirrors the server's create bar (agent can_manage_audience — the
123
+ // permission the create/apply handlers enforce on the referenced agent:
124
+ // a channel puts the agent in front of a wider audience, the owner's
125
+ // decision, not an editor's) so the connect affordance never appears to
126
+ // someone whose create would be refused.
125
127
  const { allowed: canCreate } = useCheckPermission(
126
128
  agentId ? { kind: "agent", id: agentId } : null,
127
- "can_edit",
129
+ "can_manage_audience",
128
130
  );
129
131
 
130
- // Installs go through the shared platform Slack/WhatsApp apps, a
131
- // cloud-only facility (editions program, DD-001); CRUD is not. Outside
132
- // Cloud the cards render and toggles work, but connect affordances give
133
- // way to the notice. Installs through a customer's own ChannelApp in
134
- // Enterprise are that edition's work, not assumed here.
132
+ // Installs need a channel delivery runtime, and only Stigmer Cloud
133
+ // composes one today; CRUD needs none. Outside Cloud the cards render and
134
+ // toggles work, but connect affordances give way to the notice. When a
135
+ // self-hosted edition composes a runtime, this becomes a probe of that
136
+ // runtime rather than an edition compare.
135
137
  const installsAvailable = useDeploymentMode() === "cloud";
136
138
 
137
139
  const { deleteChannel } = useDeleteAgentChannel();
@@ -175,7 +175,7 @@ function ServingAppOption({
175
175
  checked={checked}
176
176
  onChange={onSelect}
177
177
  disabled={disabled}
178
- className="stg:mt-0.5 stg:accent-current"
178
+ className="stg:mt-0.5"
179
179
  />
180
180
  <span className="stg:min-w-0">
181
181
  <span className="stg:block stg:text-xs stg:font-medium stg:text-foreground">{label}</span>