@hachej/boring-governance 0.1.83 → 0.1.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/front/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { LoadCompanyAdminStatus, RenderCompanyAdminContent, CompanyAdminLabels } from '@hachej/boring-core/front';
|
|
2
|
-
import * as
|
|
3
|
-
import {
|
|
4
|
-
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { ReactNode } from 'react';
|
|
4
|
+
import { G as GovernanceUsageSummary } from '../usageContract-DiJBjBko.js';
|
|
5
|
+
export { a as GovernanceCompanyContextAccess, b as GovernanceUsageEntry, c as GovernanceUsageRole } from '../usageContract-DiJBjBko.js';
|
|
5
6
|
|
|
6
7
|
interface GovernancePolicyStatus {
|
|
7
8
|
state: 'disabled' | 'active' | 'invalid';
|
|
@@ -43,7 +44,7 @@ interface GovernanceMeResponse {
|
|
|
43
44
|
}
|
|
44
45
|
declare function GovernanceAdminView({ status }: {
|
|
45
46
|
status: GovernanceMeResponse;
|
|
46
|
-
}):
|
|
47
|
+
}): React.JSX.Element;
|
|
47
48
|
|
|
48
49
|
interface GovernanceUsageMetersProps {
|
|
49
50
|
/** Route to fetch the usage summary from. Defaults to the governance plugin route. */
|
|
@@ -64,7 +65,35 @@ interface GovernanceUsageMetersProps {
|
|
|
64
65
|
* usage-summary route by default; renders nothing when governance is disabled so
|
|
65
66
|
* a settings host can hide the section entirely.
|
|
66
67
|
*/
|
|
67
|
-
declare function GovernanceUsageMeters({ endpoint, fetchImpl, fetchUsageSummary, title, description, className, }: GovernanceUsageMetersProps):
|
|
68
|
+
declare function GovernanceUsageMeters({ endpoint, fetchImpl, fetchUsageSummary, title, description, className, }: GovernanceUsageMetersProps): React.JSX.Element | null;
|
|
69
|
+
|
|
70
|
+
interface GovernanceUsagePanelProps {
|
|
71
|
+
/**
|
|
72
|
+
* Label for the company-context box heading, its "(read/write)" line, and the
|
|
73
|
+
* "… context paths" heading. Tenants pass their own name (e.g. "Constellation
|
|
74
|
+
* context"). Defaults to "Company context".
|
|
75
|
+
*/
|
|
76
|
+
contextLabel?: string;
|
|
77
|
+
/** Intro copy under the panel heading. Defaults to the credits-free budget line. */
|
|
78
|
+
description?: ReactNode;
|
|
79
|
+
/** Route to fetch the usage summary from. Defaults to the governance plugin route. */
|
|
80
|
+
endpoint?: string;
|
|
81
|
+
/** Override the fetch implementation (tests / non-browser hosts). */
|
|
82
|
+
fetchImpl?: typeof fetch;
|
|
83
|
+
/** Fully override data loading; takes precedence over `endpoint`/`fetchImpl`. */
|
|
84
|
+
fetchUsageSummary?: () => Promise<GovernanceUsageSummary>;
|
|
85
|
+
/** Section heading. */
|
|
86
|
+
title?: string;
|
|
87
|
+
className?: string;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Complete governed-usage settings panel: role, aggregate monthly cap,
|
|
91
|
+
* company-context access, the per-model usage meters (reusing
|
|
92
|
+
* `GovernanceUsageMeters`), and the company-context allow-rule chips. Self-fetches
|
|
93
|
+
* the summary once and hands the resolved data to the embedded meters so there is
|
|
94
|
+
* a single network round-trip. Renders nothing when governance is disabled.
|
|
95
|
+
*/
|
|
96
|
+
declare function GovernanceUsagePanel({ contextLabel, description, endpoint, fetchImpl, fetchUsageSummary, title, className, }: GovernanceUsagePanelProps): React.JSX.Element | null;
|
|
68
97
|
|
|
69
98
|
interface CreateGovernanceCompanyAdminOptions {
|
|
70
99
|
fetchImpl?: typeof fetch;
|
|
@@ -81,4 +110,4 @@ type GovernanceFrontPlugin = ((api: unknown) => void) & {
|
|
|
81
110
|
};
|
|
82
111
|
declare const governanceFrontPlugin: GovernanceFrontPlugin;
|
|
83
112
|
|
|
84
|
-
export { type CreateGovernanceCompanyAdminOptions, GovernanceAdminView, type GovernanceCompanyAdminProvider, type GovernanceMeResponse, type GovernancePolicyStatus, GovernanceUsageMeters, type GovernanceUsageMetersProps, GovernanceUsageSummary, createGovernanceCompanyAdmin, governanceFrontPlugin as default };
|
|
113
|
+
export { type CreateGovernanceCompanyAdminOptions, GovernanceAdminView, type GovernanceCompanyAdminProvider, type GovernanceMeResponse, type GovernancePolicyStatus, GovernanceUsageMeters, type GovernanceUsageMetersProps, GovernanceUsagePanel, type GovernanceUsagePanelProps, GovernanceUsageSummary, createGovernanceCompanyAdmin, governanceFrontPlugin as default };
|
package/dist/front/index.js
CHANGED
|
@@ -203,8 +203,109 @@ async function fetchSummary(endpoint, fetchImpl, signal) {
|
|
|
203
203
|
return await response.json();
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
// src/front/GovernanceUsagePanel.tsx
|
|
207
|
+
import * as React2 from "react";
|
|
208
|
+
import { Button as Button3, Chip, ErrorState as ErrorState2, LoadingState as LoadingState2 } from "@hachej/boring-ui-kit";
|
|
209
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
210
|
+
var DEFAULT_ENDPOINT2 = "/api/v1/governance/usage-summary";
|
|
211
|
+
var DEFAULT_CONTEXT_LABEL = "Company context";
|
|
212
|
+
var DEFAULT_DESCRIPTION = "This deployment uses admin-managed monthly model budgets, not prepaid credits.";
|
|
213
|
+
var MICROS_PER_EUR2 = 1e6;
|
|
214
|
+
function formatEurMicros(micros) {
|
|
215
|
+
if (micros === null) return "No aggregate cap";
|
|
216
|
+
return new Intl.NumberFormat(void 0, { style: "currency", currency: "EUR", maximumFractionDigits: 2 }).format(micros / MICROS_PER_EUR2);
|
|
217
|
+
}
|
|
218
|
+
function contextAccessLabel(summary, contextLabel) {
|
|
219
|
+
if (summary.companyContextRules.length === 0) return "Not enabled";
|
|
220
|
+
return summary.companyContextAccess === "readwrite" ? `${contextLabel} (read/write)` : "Read-only allowed paths";
|
|
221
|
+
}
|
|
222
|
+
function GovernanceUsagePanel({
|
|
223
|
+
contextLabel = DEFAULT_CONTEXT_LABEL,
|
|
224
|
+
description = DEFAULT_DESCRIPTION,
|
|
225
|
+
endpoint = DEFAULT_ENDPOINT2,
|
|
226
|
+
fetchImpl,
|
|
227
|
+
fetchUsageSummary,
|
|
228
|
+
title = "Usage limits",
|
|
229
|
+
className
|
|
230
|
+
}) {
|
|
231
|
+
const [state, setState] = React2.useState({ status: "loading" });
|
|
232
|
+
const load = React2.useCallback(async (signal) => {
|
|
233
|
+
setState({ status: "loading" });
|
|
234
|
+
try {
|
|
235
|
+
const summary2 = fetchUsageSummary ? await fetchUsageSummary() : await fetchSummary2(endpoint, fetchImpl, signal);
|
|
236
|
+
if (signal?.aborted) return;
|
|
237
|
+
setState({ status: "ready", summary: summary2 });
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (signal?.aborted) return;
|
|
240
|
+
setState({ status: "error", message: error instanceof Error ? error.message : "Failed to load usage" });
|
|
241
|
+
}
|
|
242
|
+
}, [endpoint, fetchImpl, fetchUsageSummary]);
|
|
243
|
+
React2.useEffect(() => {
|
|
244
|
+
const controller = new AbortController();
|
|
245
|
+
void load(controller.signal);
|
|
246
|
+
return () => controller.abort();
|
|
247
|
+
}, [load]);
|
|
248
|
+
if (state.status === "loading") {
|
|
249
|
+
return /* @__PURE__ */ jsx3(PanelShell, { title, description, className, children: /* @__PURE__ */ jsx3(LoadingState2, { label: "Loading governed model limits\u2026" }) });
|
|
250
|
+
}
|
|
251
|
+
if (state.status === "error") {
|
|
252
|
+
return /* @__PURE__ */ jsx3(PanelShell, { title, description, className, children: /* @__PURE__ */ jsx3(
|
|
253
|
+
ErrorState2,
|
|
254
|
+
{
|
|
255
|
+
title: "Couldn\u2019t load usage limits",
|
|
256
|
+
description: state.message,
|
|
257
|
+
actions: /* @__PURE__ */ jsx3(Button3, { variant: "outline", size: "sm", onClick: () => void load(), children: "Retry" })
|
|
258
|
+
}
|
|
259
|
+
) });
|
|
260
|
+
}
|
|
261
|
+
const { summary } = state;
|
|
262
|
+
if (!summary.enabled) return null;
|
|
263
|
+
const metersSummary = summary;
|
|
264
|
+
return /* @__PURE__ */ jsxs3(PanelShell, { title, description, className, children: [
|
|
265
|
+
/* @__PURE__ */ jsxs3("div", { className: "mt-5 grid gap-3 sm:grid-cols-3", children: [
|
|
266
|
+
/* @__PURE__ */ jsx3(StatCard, { label: "Role", value: summary.role ?? "Not governed" }),
|
|
267
|
+
/* @__PURE__ */ jsx3(StatCard, { label: "Aggregate monthly cap", value: formatEurMicros(summary.aggregateCapMicros) }),
|
|
268
|
+
/* @__PURE__ */ jsx3(StatCard, { label: contextLabel, value: contextAccessLabel(summary, contextLabel) })
|
|
269
|
+
] }),
|
|
270
|
+
/* @__PURE__ */ jsx3("div", { className: "mt-6", children: /* @__PURE__ */ jsx3(GovernanceUsageMeters, { fetchUsageSummary: async () => metersSummary }) }),
|
|
271
|
+
summary.companyContextRules.length > 0 ? /* @__PURE__ */ jsxs3("div", { className: "mt-6", children: [
|
|
272
|
+
/* @__PURE__ */ jsxs3("h3", { className: "text-sm font-medium text-foreground", children: [
|
|
273
|
+
contextLabel,
|
|
274
|
+
" paths"
|
|
275
|
+
] }),
|
|
276
|
+
/* @__PURE__ */ jsx3("div", { className: "mt-3 flex flex-wrap gap-2", children: summary.companyContextRules.map((rule) => /* @__PURE__ */ jsx3(Chip, { className: "font-mono text-muted-foreground", children: rule }, rule)) })
|
|
277
|
+
] }) : null
|
|
278
|
+
] });
|
|
279
|
+
}
|
|
280
|
+
function PanelShell({
|
|
281
|
+
title,
|
|
282
|
+
description,
|
|
283
|
+
className,
|
|
284
|
+
children
|
|
285
|
+
}) {
|
|
286
|
+
return /* @__PURE__ */ jsxs3("section", { "data-slot": "governance-usage-panel", className, children: [
|
|
287
|
+
/* @__PURE__ */ jsxs3("header", { children: [
|
|
288
|
+
/* @__PURE__ */ jsx3("h2", { className: "text-lg font-semibold text-foreground", children: title }),
|
|
289
|
+
/* @__PURE__ */ jsx3("p", { className: "mt-1 text-sm text-muted-foreground", children: description })
|
|
290
|
+
] }),
|
|
291
|
+
children
|
|
292
|
+
] });
|
|
293
|
+
}
|
|
294
|
+
function StatCard({ label, value }) {
|
|
295
|
+
return /* @__PURE__ */ jsxs3("div", { className: "rounded-xl border border-border bg-card p-4", children: [
|
|
296
|
+
/* @__PURE__ */ jsx3("div", { className: "text-xs uppercase tracking-[0.18em] text-muted-foreground", children: label }),
|
|
297
|
+
/* @__PURE__ */ jsx3("div", { className: "mt-2 text-base font-medium text-foreground", children: value })
|
|
298
|
+
] });
|
|
299
|
+
}
|
|
300
|
+
async function fetchSummary2(endpoint, fetchImpl, signal) {
|
|
301
|
+
const doFetch = fetchImpl ?? fetch;
|
|
302
|
+
const response = await doFetch(endpoint, { credentials: "include", signal });
|
|
303
|
+
if (!response.ok) throw new Error(`Usage summary failed: HTTP ${response.status}`);
|
|
304
|
+
return await response.json();
|
|
305
|
+
}
|
|
306
|
+
|
|
206
307
|
// src/front/index.tsx
|
|
207
|
-
import { jsx as
|
|
308
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
208
309
|
function createGovernanceCompanyAdmin({ fetchImpl = fetch } = {}) {
|
|
209
310
|
const loadStatus = async () => {
|
|
210
311
|
const response = await fetchImpl("/api/v1/governance/me", { credentials: "include" });
|
|
@@ -221,7 +322,7 @@ function createGovernanceCompanyAdmin({ fetchImpl = fetch } = {}) {
|
|
|
221
322
|
details: body
|
|
222
323
|
};
|
|
223
324
|
};
|
|
224
|
-
const renderContent = (status) => /* @__PURE__ */
|
|
325
|
+
const renderContent = (status) => /* @__PURE__ */ jsx4(GovernanceAdminView, { status: status.details });
|
|
225
326
|
const labels = {
|
|
226
327
|
menuLabel: "Company Admin",
|
|
227
328
|
pageTitle: "Company Admin",
|
|
@@ -236,6 +337,7 @@ var front_default = governanceFrontPlugin;
|
|
|
236
337
|
export {
|
|
237
338
|
GovernanceAdminView,
|
|
238
339
|
GovernanceUsageMeters,
|
|
340
|
+
GovernanceUsagePanel,
|
|
239
341
|
createGovernanceCompanyAdmin,
|
|
240
342
|
front_default as default
|
|
241
343
|
};
|
package/dist/server/index.d.ts
CHANGED
|
@@ -2,8 +2,8 @@ import * as _hachej_boring_agent_server from '@hachej/boring-agent/server';
|
|
|
2
2
|
import { AgentMeteringSink, RegisterAgentRoutesOptions } from '@hachej/boring-agent/server';
|
|
3
3
|
import { CoreConfig } from '@hachej/boring-core/shared';
|
|
4
4
|
import { CoreWorkspaceAgentServerPlugin } from '@hachej/boring-core/app/server';
|
|
5
|
-
import { G as GovernanceUsageSummary } from '../usageContract-
|
|
6
|
-
export {
|
|
5
|
+
import { G as GovernanceUsageSummary } from '../usageContract-DiJBjBko.js';
|
|
6
|
+
export { b as GovernanceUsageEntry } from '../usageContract-DiJBjBko.js';
|
|
7
7
|
import { PostgresBudgetReservationStore } from '@hachej/boring-core/server';
|
|
8
8
|
import { FastifyRequest } from 'fastify';
|
|
9
9
|
|
|
@@ -153,6 +153,19 @@ declare class GovernanceService {
|
|
|
153
153
|
* period boundary (never hardcoded).
|
|
154
154
|
*/
|
|
155
155
|
getUsageSummary(user: GovernanceUserLike | null | undefined, reader: GovernanceUsageSpendReader): Promise<GovernanceUsageSummary>;
|
|
156
|
+
/**
|
|
157
|
+
* Non per-model summary fields (role, aggregate cap, company-context access +
|
|
158
|
+
* rules) shared by every getUsageSummary return path and by the plugin route's
|
|
159
|
+
* empty fallback. Reuses the existing policy accessors so the panel and the
|
|
160
|
+
* admission path can never disagree.
|
|
161
|
+
*/
|
|
162
|
+
usageSummaryMeta(user: GovernanceUserLike | null | undefined): {
|
|
163
|
+
enabled: boolean;
|
|
164
|
+
role: TenantRole | null;
|
|
165
|
+
aggregateCapMicros: number | null;
|
|
166
|
+
companyContextAccess: CompanyContextAccess;
|
|
167
|
+
companyContextRules: string[];
|
|
168
|
+
};
|
|
156
169
|
me(user: GovernanceUserLike | null | undefined): GovernanceMeResponse;
|
|
157
170
|
}
|
|
158
171
|
|
package/dist/server/index.js
CHANGED
|
@@ -387,10 +387,10 @@ var GovernanceService = class {
|
|
|
387
387
|
* period boundary (never hardcoded).
|
|
388
388
|
*/
|
|
389
389
|
async getUsageSummary(user, reader) {
|
|
390
|
-
if (!this.loaded.enabled) return {
|
|
390
|
+
if (!this.loaded.enabled) return { ...this.usageSummaryMeta(user), currency: "EUR", models: [], aggregate: null };
|
|
391
391
|
const userPolicy = this.userPolicy(user);
|
|
392
392
|
const userId = user?.id;
|
|
393
|
-
if (!userPolicy || !userId) return {
|
|
393
|
+
if (!userPolicy || !userId) return { ...this.usageSummaryMeta(user), currency: "EUR", models: [], aggregate: null };
|
|
394
394
|
const models = [];
|
|
395
395
|
for (const grant of userPolicy.models) {
|
|
396
396
|
const snapshot = await reader.getSpendSnapshot({ scope: "model", userId, provider: grant.provider, model: grant.id });
|
|
@@ -414,7 +414,22 @@ var GovernanceService = class {
|
|
|
414
414
|
snapshot
|
|
415
415
|
});
|
|
416
416
|
}
|
|
417
|
-
return {
|
|
417
|
+
return { ...this.usageSummaryMeta(user), currency: "EUR", models, aggregate };
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Non per-model summary fields (role, aggregate cap, company-context access +
|
|
421
|
+
* rules) shared by every getUsageSummary return path and by the plugin route's
|
|
422
|
+
* empty fallback. Reuses the existing policy accessors so the panel and the
|
|
423
|
+
* admission path can never disagree.
|
|
424
|
+
*/
|
|
425
|
+
usageSummaryMeta(user) {
|
|
426
|
+
return {
|
|
427
|
+
enabled: this.loaded.enabled,
|
|
428
|
+
role: this.roleForUser(user),
|
|
429
|
+
aggregateCapMicros: user ? this.userMonthlyBudgetMicros(user) : null,
|
|
430
|
+
companyContextAccess: this.companyContextAccessForUser(user),
|
|
431
|
+
companyContextRules: user ? this.companyContextRules(user) : []
|
|
432
|
+
};
|
|
418
433
|
}
|
|
419
434
|
me(user) {
|
|
420
435
|
const role = this.roleForUser(user);
|
|
@@ -484,7 +499,7 @@ function governanceRoutes(service, options = {}) {
|
|
|
484
499
|
const user = request.user ?? null;
|
|
485
500
|
const reader = options.getUsageReader?.();
|
|
486
501
|
if (!user || !reader) {
|
|
487
|
-
return {
|
|
502
|
+
return { ...service.usageSummaryMeta(user), currency: "EUR", models: [], aggregate: null };
|
|
488
503
|
}
|
|
489
504
|
return service.getUsageSummary({ id: user.id, email: user.email, emailVerified: user.emailVerified }, reader);
|
|
490
505
|
});
|
|
@@ -18,6 +18,10 @@ interface GovernanceUsageEntry {
|
|
|
18
18
|
/** ISO timestamp of the budget-period reset boundary. */
|
|
19
19
|
resetsAt: string;
|
|
20
20
|
}
|
|
21
|
+
/** Caller's governance role, or null when the caller is not governed. */
|
|
22
|
+
type GovernanceUsageRole = 'admin' | 'user' | null;
|
|
23
|
+
/** Caller's access to the company-context workspace. */
|
|
24
|
+
type GovernanceCompanyContextAccess = 'none' | 'readonly' | 'readwrite';
|
|
21
25
|
interface GovernanceUsageSummary {
|
|
22
26
|
/** Whether governance is enabled for this host. */
|
|
23
27
|
enabled: boolean;
|
|
@@ -27,6 +31,14 @@ interface GovernanceUsageSummary {
|
|
|
27
31
|
models: GovernanceUsageEntry[];
|
|
28
32
|
/** Aggregate ("All models") row when an aggregate cap is configured, else null. */
|
|
29
33
|
aggregate: GovernanceUsageEntry | null;
|
|
34
|
+
/** Caller's governance role, or null when not governed. */
|
|
35
|
+
role: GovernanceUsageRole;
|
|
36
|
+
/** Aggregate monthly cap in micros, or null when no aggregate cap is configured. */
|
|
37
|
+
aggregateCapMicros: number | null;
|
|
38
|
+
/** Caller's access to the company-context workspace. */
|
|
39
|
+
companyContextAccess: GovernanceCompanyContextAccess;
|
|
40
|
+
/** Company-context allow-rule patterns granted to the caller. */
|
|
41
|
+
companyContextRules: string[];
|
|
30
42
|
}
|
|
31
43
|
|
|
32
|
-
export type { GovernanceUsageSummary as G,
|
|
44
|
+
export type { GovernanceUsageSummary as G, GovernanceCompanyContextAccess as a, GovernanceUsageEntry as b, GovernanceUsageRole as c };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hachej/boring-governance",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.85",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"react": "^18.0.0 || ^19.0.0",
|
|
44
44
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
45
|
-
"@hachej/boring-agent": "0.1.
|
|
46
|
-
"@hachej/boring-bash": "0.1.
|
|
47
|
-
"@hachej/boring-core": "0.1.
|
|
48
|
-
"@hachej/boring-ui-kit": "0.1.
|
|
45
|
+
"@hachej/boring-agent": "0.1.85",
|
|
46
|
+
"@hachej/boring-bash": "0.1.85",
|
|
47
|
+
"@hachej/boring-core": "0.1.85",
|
|
48
|
+
"@hachej/boring-ui-kit": "0.1.85"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"proper-lockfile": "^4.1.2",
|
|
@@ -67,10 +67,10 @@
|
|
|
67
67
|
"tsx": "^4.20.6",
|
|
68
68
|
"typescript": "~6.0.3",
|
|
69
69
|
"vitest": "^4.1.9",
|
|
70
|
-
"@hachej/boring-agent": "0.1.
|
|
71
|
-
"@hachej/boring-bash": "0.1.
|
|
72
|
-
"@hachej/boring-
|
|
73
|
-
"@hachej/boring-
|
|
70
|
+
"@hachej/boring-agent": "0.1.85",
|
|
71
|
+
"@hachej/boring-bash": "0.1.85",
|
|
72
|
+
"@hachej/boring-ui-kit": "0.1.85",
|
|
73
|
+
"@hachej/boring-core": "0.1.85"
|
|
74
74
|
},
|
|
75
75
|
"scripts": {
|
|
76
76
|
"build": "tsup",
|