Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@equinor/fusion-framework-dev-portal": patch
---

Integrate `@equinor/fusion-framework-plugin-context-navigation` into the dev portal.

Portal context-to-URL reconciliation is now handled by `@equinor/fusion-framework-plugin-context-navigation`, replacing the ad-hoc hook-based approach.
16 changes: 16 additions & 0 deletions .changeset/fusion-framework-module-context_routing-strategy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"@equinor/fusion-framework-module-context": patch
---

Move URL-based initial context resolution to the context-navigation plugin.

The context module's `resolveInitialContext` no longer resolves context from the URL path — that responsibility has been moved to `@equinor/fusion-framework-plugin-context-navigation` at the portal level. This decouples URL concerns from the context module.

**Changes:**

- `resolveInitialContext` no longer accepts the `options` parameter with path resolution config.
- URL-based initial context resolution is now handled by the context-navigation plugin.
- Added `version` property to `IContextProvider` interface (non-breaking).
- Added explanatory `@ts-ignore` directives on compatibility assignments to avoid a breaking API signature change in this release.

**Migration:** Apps do not need to change their code. Portal hosts should enable the `@equinor/fusion-framework-plugin-context-navigation` plugin to restore URL-based context resolution behavior.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@equinor/fusion-framework-module-navigation": patch
---

Fix basename boundary matching and trailing-slash handling.

- `normalizePathname` no longer strips trailing slashes — only collapses consecutive slashes. Trailing slash is now preserved as part of the path identity.
- `_isWithinBasenameScope` uses a path-boundary check (`pathname === basename || pathname.startsWith(basename + '/')`) to prevent false positives from apps with overlapping name prefixes (e.g. `/apps/my-app` no longer matches `/apps/my-app-other/foo`).
- `_localizePath` falls back to `'/'` when the basename-stripped pathname is empty.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
"@equinor/fusion-framework-plugin-context-navigation": minor
---

Initial release of the context navigation plugin.

Adapter-based, event-driven plugin that reconciles context selection with the browser URL for portal hosts. Ships with built-in adapters for query-param, path-segment, and custom URL shapes, plus two pre-wired source strategies:

- **app-first** — app sets context, the plugin encodes it to the URL.
- **context-first** — the plugin decodes context from the URL on startup, redirects to a configurable null-context URL when no context is resolvable.
Comment thread
Noggling marked this conversation as resolved.

```ts
import { enableContextNavigation } from '@equinor/fusion-framework-plugin-context-navigation';
import { createAppFirstSource } from '@equinor/fusion-framework-plugin-context-navigation/sources';

enableContextNavigation(configurator, (builder) => {
builder.setSourceFactory(createAppFirstSource());
});
```
11 changes: 11 additions & 0 deletions .changeset/module-navigation_fix-redos-vulnerability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@equinor/fusion-framework-module-navigation": patch
---

**Security Fix**: Replaced regex-based pathname normalization with iterative approach to prevent potential ReDoS (Regular Expression Denial of Service) vulnerability when processing user-controlled basename values.

The `normalizePathname` function now uses a simple character-by-character scan instead of `/\/+/g` regex, ensuring O(n) linear time complexity even with pathological input containing thousands of consecutive slashes.

This addresses CodeQL security alert: "Polynomial regular expression used on uncontrolled data"

Related: #4751
10 changes: 8 additions & 2 deletions CODEMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ search to rediscover it.

| Path | Contains | Published? |
| --- | --- | --- |
| `packages/*` | Framework libraries (58 packages) | Yes, via Changesets |
| `packages/*` | Framework libraries (59 packages) | Yes, via Changesets |
| `cookbooks/*` | Runnable example apps and portals | Yes (versioned, but examples) |
| `eds-content/`, `eds/` | EDS design-system content and token tooling | No |
| `vue-press/` | Documentation site | Partly |
Expand Down Expand Up @@ -82,7 +82,13 @@ Format: `package name` → path → role.
| `@equinor/fusion-framework-react-components-bookmark` | `packages/react/components/bookmark` | Bookmark UI components |
| `@equinor/fusion-framework-react-components-people-provider` | `packages/react/components/people-resolver` | People resolver components |

### Utils (`packages/utils/*`)
### Plugins (`packages/plugins/*`)i

| Package | Path | Role |
| --- | --- | --- |
| `@equinor/fusion-framework-plugin-context-navigation` | `packages/plugins/context-navigation` | Plugin for context-based navigation handling |

## Utils (`packages/utils/*`)

| Package | Path | Role |
| --- | --- | --- |
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"lint:fusion": "pnpm build:lint && pnpm lint:fusion:packages && pnpm lint:fusion:cookbooks",
"lint:fusion:packages": "fusion-lint lint packages",
"lint:fusion:cookbooks": "fusion-lint lint cookbooks",
"lint:fusion:plugins": "fusion-lint lint packages/plugins",
"lint:staged": "biome lint --staged",
"verify:agent-context": "node .github/scripts/verify-agent-context.mjs",
"format": "biome format",
Expand Down
7 changes: 4 additions & 3 deletions packages/dev-portal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Use this package when you need a portal host for local app development. For the
- **Portal shell**: A React application that renders the top bar, context selector, and an app mounting area.
- **Application loader**: Dynamically initializes and mounts a Fusion app by its `appKey`, handling manifest resolution, script loading, and teardown.
- **Framework modules**: The portal pre-configures telemetry, navigation, bookmarks, feature flags, analytics, AG Grid, and service integrations so loaded apps inherit a realistic environment.
- **Context navigation**: When an app uses the context module, the portal synchronizes URL navigation with context changes automatically.
- **Context navigation plugin**: The portal enables `enableContextNavigation` from `@equinor/fusion-framework-plugin-context-navigation` so context changes and app switches keep the URL in sync automatically.

## Installation

Expand Down Expand Up @@ -94,12 +94,13 @@ const devServer = await createDevServer({
The portal is composed of these internal parts:

- **`render`** — Entry point; creates a React root with theme, framework, and people-resolver providers.
- **`configure`** — Configures all framework modules (telemetry, navigation, bookmarks, feature flags, analytics, AG Grid, services).
- **`configure`** — Configures all framework modules (telemetry, navigation, context navigation, bookmarks, feature flags, analytics, AG Grid, services).
- **Context configuration** — `enableContext` wires the context module's path generator and path extractor to the shared context-navigation URL utilities.
- **`Router`** — Sets up routes with `react-router` via the navigation module; routes `/apps/:appKey/*` to the app loader.
- **`AppLoader`** — Resolves, initializes, and mounts a Fusion app by key; handles loading states and errors.
- **`Header`** — Top bar with the Fusion logo, context selector, bookmark toggle, and person settings.
- **`ContextSelector`** — Wired to the current app's context module for searching and selecting context items.
- **`useAppContextNavigation`** — Synchronizes URL pathname with context changes for apps that use the context module.
- **`enableContextNavigation`** — Registers the portal-level context navigation plugin that reconciles the active context with the browser URL.

## Constraints

Expand Down
4 changes: 4 additions & 0 deletions packages/dev-portal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@equinor/fusion-framework-module-app": "workspace:*",
"@equinor/fusion-framework-module-bookmark": "workspace:*",
"@equinor/fusion-framework-module-context": "workspace:*",
"@equinor/fusion-framework-plugin-context-navigation": "workspace:*",
"@equinor/fusion-framework-module-feature-flag": "workspace:*",
"@equinor/fusion-framework-module-navigation": "workspace:*",
"@equinor/fusion-framework-module-services": "workspace:*",
Expand All @@ -53,12 +54,14 @@
"@equinor/fusion-wc-person": "^3.5.5",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@types/semver": "^7.7.1",
"@vitejs/plugin-react": "^6.0.1",
"dotenv": "^17.3.1",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"rxjs": "^7.8.1",
"styled-components": "^6.3.11",
"semver": "^7.7.2",
"tsx": "^4.19.3",
"typescript": "^7.0.2",
"vite": "^8.0.0"
Expand All @@ -72,6 +75,7 @@
"@equinor/fusion-framework-module-app": "workspace:*",
"@equinor/fusion-framework-module-bookmark": "workspace:*",
"@equinor/fusion-framework-module-context": "workspace:*",
"@equinor/fusion-framework-plugin-context-navigation": "workspace:*",
"@equinor/fusion-framework-module-feature-flag": "workspace:*",
"@equinor/fusion-framework-module-navigation": "workspace:*",
"@equinor/fusion-framework-module-services": "workspace:*",
Expand Down
9 changes: 3 additions & 6 deletions packages/dev-portal/src/Router.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { useBookmarkNavigate } from '@equinor/fusion-framework-react-module-bookmark/portal';

import { Outlet, Router as FusionRouter, useParams } from '@equinor/fusion-framework-react-router';
import { Router as FusionRouter, Outlet, useParams } from '@equinor/fusion-framework-react-router';
import AppLoader from './AppLoader';
import { Header } from './Header';

import { styled } from 'styled-components';
import { useAppContextNavigation } from './useAppContextNavigation';

const Styled = {
ContentContainer: styled.div`
Expand Down Expand Up @@ -76,11 +75,9 @@ const routes = [
/**
* Top-level router for the Fusion Dev Portal.
*
* Renders the application via `FusionRouter`. Observes context changes through
* {@link useAppContextNavigation} to keep the URL in sync.
* Uses `@equinor/fusion-framework-react-router` which automatically connects
* to the framework's navigation module for history and basename.
*/
export const Router = () => {
// observe the context changes and navigate when the context changes
useAppContextNavigation();
return <FusionRouter routes={routes} />;
};
79 changes: 60 additions & 19 deletions packages/dev-portal/src/configure.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,36 @@
import { enableAppModule } from '@equinor/fusion-framework-module-app';
import { enableAppModule, type AppModule } from '@equinor/fusion-framework-module-app';
import { enableBookmark } from '@equinor/fusion-framework-react-module-bookmark';
import type { FrameworkConfigurator } from '@equinor/fusion-framework';
import { enableAnalytics } from '@equinor/fusion-framework-module-analytics';
import { ConsoleAnalyticsAdapter } from '@equinor/fusion-framework-module-analytics/adapters';
import { enableNavigation } from '@equinor/fusion-framework-module-navigation';
import { enableContext } from '@equinor/fusion-framework-module-context';
import {
enableNavigation,
type NavigationModule,
} from '@equinor/fusion-framework-module-navigation';
import { enableServices } from '@equinor/fusion-framework-module-services';
import { enableFeatureFlagging } from '@equinor/fusion-framework-module-feature-flag';
import {
createLocalStoragePlugin,
createUrlPlugin,
} from '@equinor/fusion-framework-module-feature-flag/plugins';
import { enableAgGrid } from '@equinor/fusion-framework-module-ag-grid';

import { enableTelemetry } from '@equinor/fusion-framework-module-telemetry';
import {
enableContextNavigation,
legacyAppNavigationFix,
} from '@equinor/fusion-framework-plugin-context-navigation';
import {
buildContextUrlForStrategy,
resolveContextIdFromUrl,
} from '@equinor/fusion-framework-plugin-context-navigation/utils';
import { version } from './version';

declare global {
interface Window {
/**
* AG Grid license key for enabling enterprise features
* @remarks This is typically set via environment variables during build time
* AG Grid license key for enabling enterprise features.
* @remarks Typically set via environment variables during build time.
*/
FUSION_AG_GRID_KEY?: string;
}
Expand All @@ -28,10 +39,15 @@ declare global {
/**
* Configures the Fusion Dev Portal framework with all required modules.
*
* Enables and wires together:
* Modules enabled:
* - **Telemetry** — portal-scoped usage analytics with version metadata.
* - **App module** — application manifest loading and lifecycle.
* - **Navigation** — router integration with optional telemetry.
* - **App** — application manifest loading and lifecycle.
* - **Context** — context routing URL hooks (path generator + extractor) wired to the shared context-navigation URL utilities.
* - **Context Navigation plugin** — keeps the browser URL in sync with the
* active context, handles app-switch carry-over, and guards against
* accidental context loss. Telemetry is auto-resolved from the framework
* telemetry module.
* - **Navigation** — router integration with telemetry.
* - **Services** — standard Fusion service integrations.
* - **AG Grid** — enterprise license key from `window.FUSION_AG_GRID_KEY`.
* - **Analytics** — console adapter gated by the `fusionLogAnalytics` feature flag.
Expand All @@ -51,10 +67,7 @@ export const configure = async (config: FrameworkConfigurator) => {
builder.setMetadata(() => ({
fusion: {
type: 'portal-telemetry',
portal: {
version,
name: 'Fusion Dev Portal',
},
portal: { version, name: 'Fusion Dev Portal' },
},
}));
// Scope telemetry events to portal-specific tracking
Expand All @@ -66,8 +79,26 @@ export const configure = async (config: FrameworkConfigurator) => {

enableAppModule(config);

/**
* Configure context module with dev-portal URL conventions.
*
* This wires the context module's URL hooks — the path generator
* and path extractor — to the dev-portal's URL routing strategy.
*
* The context-navigation plugin keeps the browser URL in sync with the
* active context as it changes at runtime.
*/
enableContext(config, (builder) => {
builder.setContextPathGenerator((context, path) =>
buildContextUrlForStrategy(context?.id, path),
);

builder.setContextPathExtractor((path) => resolveContextIdFromUrl(path));
});

enableNavigation(config, {
configure: (config) => {
config.setBasename('/');
config.setTelemetry(async (args) => {
// Only provide telemetry when the telemetry module was actually enabled
if (args.hasModule('telemetry')) {
Expand Down Expand Up @@ -133,13 +164,23 @@ export const configure = async (config: FrameworkConfigurator) => {
builder.addPlugin(createUrlPlugin(['fusionDebug']));
});

// Expose framework modules globally for development debugging and inspection
config.onInitialized(async (modules) => {
// NOTE: TypeScript ignore needed due to window object extension
// This provides developer access to all initialized modules via window.Fusion
// @ts-expect-error
// Keep portal URLs aligned with the active app/context combination by using
// the shared context-navigation plugin and the dev-portal URL helpers.
enableContextNavigation(config, (builder) => {
builder.setPortalName('dev-portal');
builder.setDebug(true);
builder.setUrlGuard(true);
builder.setNavigationOptions({
replace: false, // Use pushState for navigation to allow back button support
});
});

config.onInitialized<[AppModule, NavigationModule]>((modules) => {
// Reset legacy app routers on context navigation for apps with navigation <v7.
legacyAppNavigationFix({ event: modules.event });

// Expose framework modules globally for development debugging and inspection.
// @ts-expect-error — `window` is not typed with `Fusion`
window.Fusion = { modules };
});
};

export default configure;
Loading
Loading