chore: consolidate platform split checks
Some checks failed
Dependency Audit / dependency-audit (push) Has been cancelled
Module Matrix / module-matrix (push) Has been cancelled

This commit is contained in:
2026-07-10 12:51:19 +02:00
parent 150b720f12
commit 635d25c74c
216 changed files with 23336 additions and 4077 deletions

View File

@@ -1,19 +1,23 @@
import { Activity, CalendarDays, FileText, Folder, Form, LayoutDashboard, Mail, Mails, Shield, Users, type LucideIcon } from "lucide-react";
import installedWebModules from "virtual:govoplan-installed-modules";
import type { AuthInfo, PlatformModuleInfo, PlatformNavItem, PlatformWebModule } from "../types";
import type { AuthInfo, DashboardWidgetContribution, DashboardWidgetsUiCapability, PlatformModuleInfo, PlatformNavItem, PlatformWebModule } from "../types";
import {
hasUiCapability as hasUiCapabilityForModules,
moduleInstalled as moduleInstalledForModules,
moduleIntegrationEnabled as moduleIntegrationEnabledForModules,
routeContributionsForModules as routeContributionsForModuleList,
uiCapabilities as uiCapabilitiesForModules,
uiCapability as uiCapabilityForModules
} from "./moduleLogic";
uiCapability as uiCapabilityForModules } from
"./moduleLogic";
import { hasAnyScope, hasScope } from "../utils/permissions";
export const shellNavItems: PlatformNavItem[] = [
{ to: "/dashboard", label: "Dashboard", iconName: "dashboard", order: 10 }
];
export const fallbackDashboardNavItem: PlatformNavItem =
{ to: "/dashboard", label: "i18n:govoplan-core.dashboard.d87f47b4", iconName: "dashboard", order: 10 };
export function shellNavItemsForModules(modules: PlatformWebModule[]): PlatformNavItem[] {
return moduleInstalledForModules("dashboard", modules) ? [] : [fallbackDashboardNavItem];
}
const localModules: PlatformWebModule[] = installedWebModules;
const remoteModuleCache = new Map<string, Promise<PlatformWebModule | null>>();
@@ -104,19 +108,19 @@ export function resolveInstalledWebModules(platformModules: PlatformModuleInfo[]
if (!platformModules?.length) return localModules;
const localById = new Map(localModules.map((module) => [module.id, module]));
return platformModules
.filter((module) => module.enabled)
.map((module) => {
const local = localById.get(module.id);
return local ? applyServerMetadata(local, module) : null;
})
.filter((module): module is PlatformWebModule => module !== null);
return platformModules.
filter((module) => module.enabled).
map((module) => {
const local = localById.get(module.id);
return local ? applyServerMetadata(local, module) : null;
}).
filter((module): module is PlatformWebModule => module !== null);
}
export async function loadRemoteWebModules(
platformModules: PlatformModuleInfo[] | null | undefined,
alreadyLoaded: PlatformWebModule[] = resolveInstalledWebModules(platformModules)
): Promise<PlatformWebModule[]> {
platformModules: PlatformModuleInfo[] | null | undefined,
alreadyLoaded: PlatformWebModule[] = resolveInstalledWebModules(platformModules))
: Promise<PlatformWebModule[]> {
if (!platformModules?.length) return [];
const loadedIds = new Set(alreadyLoaded.map((module) => module.id));
const candidates = platformModules.filter((module) => {
@@ -147,7 +151,7 @@ async function loadRemoteWebModule(info: PlatformModuleInfo): Promise<PlatformWe
const manifestBuffer = await manifestResponse.arrayBuffer();
if (frontend.asset_manifest_integrity) {
const ok = await verifyIntegrity(manifestBuffer, frontend.asset_manifest_integrity);
if (!ok) throw new Error("manifest integrity check failed");
if (!ok) throw new Error("i18n:govoplan-core.manifest_integrity_check_failed.6e3913bc");
}
const manifestText = new TextDecoder().decode(manifestBuffer);
if (frontend.asset_manifest_signature && frontend.asset_manifest_public_key_id) {
@@ -156,28 +160,28 @@ async function loadRemoteWebModule(info: PlatformModuleInfo): Promise<PlatformWe
frontend.asset_manifest_signature,
frontend.asset_manifest_public_key_id
);
if (signatureOk === false) throw new Error("manifest signature check failed");
if (signatureOk === null && !frontend.asset_manifest_integrity) throw new Error("manifest signature key is not trusted by this shell");
if (signatureOk === false) throw new Error("i18n:govoplan-core.manifest_signature_check_failed.a9860176");
if (signatureOk === null && !frontend.asset_manifest_integrity) throw new Error("i18n:govoplan-core.manifest_signature_key_is_not_trusted_by_this_sh.3bb2f629");
} else if (!frontend.asset_manifest_integrity) {
throw new Error("remote manifests need an integrity hash or a verifiable signature");
throw new Error("i18n:govoplan-core.remote_manifests_need_an_integrity_hash_or_a_ver.bfbb7daa");
}
const manifest = JSON.parse(manifestText) as RemoteAssetManifest;
const contractVersion = manifest.contractVersion ?? frontend.asset_manifest_contract_version ?? "1";
if (contractVersion !== "1") throw new Error(`unsupported remote asset contract ${contractVersion}`);
if (manifest.moduleId && manifest.moduleId !== info.id) throw new Error(`manifest module id ${manifest.moduleId} does not match ${info.id}`);
if (!manifest.entry) throw new Error("remote manifest has no entry");
if (!manifest.entryIntegrity) throw new Error("remote entry needs an integrity hash");
if (!manifest.entry) throw new Error("i18n:govoplan-core.remote_manifest_has_no_entry.8220dbc9");
if (!manifest.entryIntegrity) throw new Error("i18n:govoplan-core.remote_entry_needs_an_integrity_hash.3b69ae0a");
const entryUrl = absoluteModuleAssetUrl(manifest.entry, manifestUrl);
const entryResponse = await fetch(entryUrl, { credentials: "same-origin" });
if (!entryResponse.ok) throw new Error(`entry fetch failed with HTTP ${entryResponse.status}`);
const entryBuffer = await entryResponse.arrayBuffer();
if (!(await verifyIntegrity(entryBuffer, manifest.entryIntegrity))) throw new Error("entry integrity check failed");
if (!(await verifyIntegrity(entryBuffer, manifest.entryIntegrity))) throw new Error("i18n:govoplan-core.entry_integrity_check_failed.1d8f6b89");
const blobUrl = URL.createObjectURL(new Blob([entryBuffer], { type: "text/javascript" }));
try {
const imported = await import(/* @vite-ignore */ blobUrl) as Record<string, unknown>;
const imported = (await import(/* @vite-ignore */blobUrl)) as Record<string, unknown>;
const exported = manifest.moduleExport ? imported[manifest.moduleExport] : imported.default ?? imported.module;
if (!isPlatformWebModule(exported)) throw new Error("entry did not export a PlatformWebModule");
if (!isPlatformWebModule(exported)) throw new Error("i18n:govoplan-core.entry_did_not_export_a_platformwebmodule.9a157741");
if (exported.id !== info.id) throw new Error(`entry module id ${exported.id} does not match ${info.id}`);
return applyServerMetadata(exported, info);
} finally {
@@ -278,10 +282,16 @@ export function routeContributionsForModules(modules: PlatformWebModule[]) {
return routeContributionsForModuleList(modules);
}
export function dashboardWidgetsForModules(modules: PlatformWebModule[] = localModules): DashboardWidgetContribution[] {
return uiCapabilitiesForModules<DashboardWidgetsUiCapability>("dashboard.widgets", modules).
flatMap((capability) => capability.widgets ?? []).
sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
}
export function navItemsForModules(modules: PlatformWebModule[]): PlatformNavItem[] {
return [...shellNavItems, ...modules.flatMap((module) => module.navItems ?? [])]
.map(resolveNavItemIcon)
.sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
return [...shellNavItemsForModules(modules), ...modules.flatMap((module) => module.navItems ?? [])].
map(resolveNavItemIcon).
sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
}
export function visibleNavItems(auth: AuthInfo | null | undefined, modules: PlatformWebModule[] = localModules): PlatformNavItem[] {
@@ -293,7 +303,5 @@ export function visibleNavItems(auth: AuthInfo | null | undefined, modules: Plat
}
export function firstAccessibleRoute(auth: AuthInfo, modules: PlatformWebModule[] = localModules): string {
const preferred = visibleNavItems(auth, modules).find((item) => item.to !== "/dashboard");
if (preferred) return preferred.to;
return "/dashboard";
return visibleNavItems(auth, modules)[0]?.to ?? "/dashboard";
}