feat(webui): govern actions, quick access, and metrics

Refs #264, #285, #289
This commit is contained in:
2026-08-19 18:47:46 +02:00
parent 41db78c201
commit ffaab543d2
31 changed files with 753 additions and 125 deletions
+64 -4
View File
@@ -1,6 +1,7 @@
import type { AuthInfo } from "../src/types";
import type { AuthInfo, QuickAccessResult } from "../src/types";
import {
createQuickAccessLaunchContext,
isQuickAccessResultAllowed,
quickAccessLaunchContextFromState,
quickAccessLaunchState,
quickAccessReturnPath
@@ -57,15 +58,21 @@ const context = createQuickAccessLaunchContext({
locked: false,
availableViews: [],
provenance: [],
diagnostics: []
diagnostics: [],
presentation: {
quickAccessRecommendedToolIds: ["mail.messages"],
quickAccessFocusedToolIds: ["mail.messages", "files.recent"]
}
}
});
assert(context.contractVersion === "1", "launch context must be explicitly versioned");
assert(context.contractVersion === "2", "launch context must be explicitly versioned");
assert(context.referenceContractVersion === "1", "reference payloads must be independently versioned");
assert(context.activeObject?.objectId === "case-1", "active object reference must survive launch");
assert(context.actingContext?.assignmentId === "assignment-1", "acting assignment must survive launch");
assert(context.temporalContext.validityMode === "at", "temporal selection must survive launch");
assert(context.viewContext?.revisionId === "view-revision-3", "exact View revision must survive launch");
assert(context.viewContext?.recommendedToolIds[0] === "mail.messages", "View recommendations must survive launch without becoming authority");
assert(
quickAccessReturnPath(context) === "/cases/case-1?tab=history#revision-4",
"return path must preserve route, query and fragment"
@@ -75,7 +82,7 @@ assert(
"router state must decode a valid launch context"
);
assert(
quickAccessLaunchContextFromState({ govoplanQuickAccessLaunch: { contractVersion: "2" } }) === null,
quickAccessLaunchContextFromState({ govoplanQuickAccessLaunch: { contractVersion: "3" } }) === null,
"unknown launch context versions must fail closed"
);
@@ -86,3 +93,56 @@ const crossTenant = createQuickAccessLaunchContext({
temporalContext: { validityMode: "current", validAt: null, recordedAt: null }
});
assert(crossTenant.activeObject === null, "cross-tenant object references must be discarded");
const selectedFileResult = {
contractVersion: "1",
outcome: "completed",
action: "selected",
reference: {
ownerModule: "files",
kind: "file-version",
objectId: "version-1",
tenantId: "tenant-1",
label: "Permit evidence.pdf",
path: "/files?version=version-1"
}
} as const;
assert(
isQuickAccessResultAllowed(selectedFileResult, context, ["files.file-version"]),
"a declared same-tenant result reference is accepted"
);
assert(
!isQuickAccessResultAllowed(
{ ...selectedFileResult, reference: { ...selectedFileResult.reference, tenantId: "tenant-2" } },
context,
["files.file-version"]
),
"cross-tenant result references fail closed"
);
assert(
!isQuickAccessResultAllowed(selectedFileResult, context, ["records.record"]),
"undeclared result-reference kinds fail closed"
);
assert(
isQuickAccessResultAllowed({ contractVersion: "1", outcome: "cancelled", reason: "user" }, context),
"explicit cancellation is a valid terminal result"
);
assert(
!isQuickAccessResultAllowed(
{ ...selectedFileResult, outcome: "unknown" } as unknown as QuickAccessResult,
context,
["files.file-version"]
),
"unknown result outcomes fail closed"
);
assert(
!isQuickAccessResultAllowed(
{
...selectedFileResult,
reference: { ...selectedFileResult.reference, path: "//outside.example/files/version-1" }
},
context,
["files.file-version"]
),
"protocol-relative result paths fail closed"
);