Skip to content

feat(context-navigation): add plugin package, dev portal integration, and plugin docs - #4751

Merged
odinr merged 15 commits into
mainfrom
feat/context-navigation-plugin
Aug 5, 2026
Merged

feat(context-navigation): add plugin package, dev portal integration, and plugin docs#4751
odinr merged 15 commits into
mainfrom
feat/context-navigation-plugin

Conversation

@Noggling

@Noggling Noggling commented May 26, 2026

Copy link
Copy Markdown
Collaborator

Why is this change needed?
This branch introduces the context-navigation plugin as a first-class plugin package, wires it into the dev portal, and provides documentation and migration guidance. The plugin decouples URL-to-context reconciliation logic from the context module, making it an opt-in portal-level concern rather than baking it into every app's context configuration.

What is the current behavior?
On the main branch:

  • The context module attempts to resolve initial context from the URL path
  • Dev portal uses an ad-hoc useAppContextNavigation hook for URL synchronization
  • No standardized, adapter-based approach for different URL encoding strategies

What is the new behavior?
This PR adds:

  • @equinor/fusion-framework-plugin-context-navigation — an event-driven plugin that reconciles context with URLs using pluggable adapters (path, query, custom)
  • Dev portal integration — replaces useAppContextNavigation with the plugin
  • Plugin docs and cookbook examples showing how to configure routing strategies
  • Navigation module bug fixes for trailing-slash handling and basename boundary checks

What is the intended behavior or invariant?
Context-to-URL synchronization should be:

  • Portal-level — enabled via plugin, not baked into every app's context config
  • Adapter-based — supports path, query, and custom URL shapes via self-selecting adapters
  • Event-driven — reacts to context changes and app switches, with cancelable lifecycle events
  • Explicit — apps declare their preferred routing strategy in the manifest's build.options.contextRouting

Does this PR introduce a breaking change?
No breaking changes compared to main:

  • Context module changes are patch-level (URL resolution moved to plugin, apps unaffected)
  • Navigation module changes are patch-level bug fixes
  • Plugin is a new package (no prior API to break)
  • Dev portal changes are internal implementation (not a published API surface)

Impact assessment:

  • Breaking changes: None (compared to main branch)
  • Version bumps:
    • @equinor/fusion-framework-plugin-context-navigation: 0.0.00.1.0 (initial minor release)
    • @equinor/fusion-framework-module-context: patch (URL resolution moved to plugin)
    • @equinor/fusion-framework-module-navigation: patch (trailing-slash and basename fixes)
    • @equinor/fusion-framework-dev-portal: patch (internal plugin integration)
  • Consumer impact:
    • Portal hosts can install @equinor/fusion-framework-plugin-context-navigation and enable it
    • Apps declare build.options.contextRouting in manifests to control URL encoding
    • Apps with custom URL shapes register setContextPathExtractor / setContextPathGenerator hooks (unchanged from current behavior)

Review guidance:

  1. Review the plugin package structure and adapter resolution logic first
  2. Check the dev-portal integration and removal of useAppContextNavigation
  3. Verify the docs migration from /modules/context to /plugins/context-navigation
  4. Review the navigation module bug fixes (trailing-slash, basename boundary)

Additional context
Validation completed during this branch work:

  • pnpm --filter @equinor/fusion-framework-dev-portal exec tsc --noEmit -p tsconfig.json
  • Focused editor diagnostics on touched TypeScript and markdown files ✓
  • Repository searches to verify no orphaned references to removed hooks ✓

Validation not completed:

  • Full workspace pnpm test && pnpm build && pnpm -w check (CI will validate)
  • VuePress build (docs site)
  • Plugin package build in isolation

Related issues
None.

Checklist

  • Confirm completion of the self-review checklist
  • Confirm TSDoc captures intent for functions, hooks, components, classes, and named arrow functions
  • Confirm iterator blocks, decision gates, RxJS chains, and complex decisions explain why they exist
  • Confirm React logic and derived values are resolved before markup when applicable
  • Confirm README/docs are updated for user-facing changes
  • Confirm changes to target branch validation
    • Focused validation completed as noted above
    • Full workspace validation deferred to CI
  • Confirm adherence to code of conduct

Closes: https://github.com/equinor/fusion-core-tasks/issues/567

@github-actions github-actions Bot added 🐞 bug Something isn't working 👨🏻‍🍳 cookbooks 📚 documentation Improvements or additions to documentation 🚀 feature New feature or request 🚧 chore maintaines work, (update deps, workflos ...) 🧬 Modules labels May 26, 2026
@changeset-bot

changeset-bot Bot commented May 26, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 477481f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 12 packages
Name Type
@equinor/fusion-framework-dev-portal Major
@equinor/fusion-framework-module-context Patch
@equinor/fusion-framework-module-navigation Patch
@equinor/fusion-framework-plugin-context-navigation Minor
@equinor/fusion-framework-cli Patch
@equinor/fusion-framework Patch
@equinor/fusion-framework-module-analytics Patch
portal-analytics Patch
@equinor/fusion-framework-react-app Patch
poc-portal Patch
portal Patch
@equinor/fusion-framework-app Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@Noggling
Noggling marked this pull request as ready for review May 26, 2026 10:29
@Noggling
Noggling requested a review from a team as a code owner May 26, 2026 10:29
@Noggling
Noggling requested review from Copilot and odinr May 26, 2026 10:29
@Noggling Noggling self-assigned this May 26, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request introduces @equinor/fusion-framework-plugin-context-navigation as a first-class plugin package, wires it into the dev portal, and restructures VuePress docs to present context navigation under a new /plugins/ surface (instead of module-oriented paths). It also extends the context and navigation modules to support routing strategy semantics and basename/path normalization required by the new plugin model.

Changes:

  • Add new context-navigation plugin package (runtime, configurator, adapters, sources, URL utilities, events) plus initial changeset.
  • Integrate the plugin into @equinor/fusion-framework-dev-portal and remove the prior ad-hoc useAppContextNavigation hook.
  • Add/adjust docs and migrations (VuePress plugin section + context routing strategy migration), plus context/navigation module behavior updates and changesets.

Reviewed changes

Copilot reviewed 59 out of 60 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
vue-press/src/plugins/README.md New VuePress landing page for plugins and plugin intent.
vue-press/src/plugins/context-navigation/README.md VuePress entry for context-navigation plugin, includes package README.
vue-press/src/modules/context/migration-routing-strategy.md VuePress wrapper for context routing strategy migration doc.
vue-press/src/.vuepress/theme.ts Adds “Plugins” to top navbar.
vue-press/src/.vuepress/sidebar.ts Adds /plugins/ sidebar section and context-navigation entry.
packages/plugins/context-navigation/vitest.config.ts Adds Vitest project config for the new plugin package.
packages/plugins/context-navigation/tsconfig.json Adds TS project config + references for plugin package.
packages/plugins/context-navigation/src/utils/url/url-utils.ts URL strategy dispatch + provider-level URL helpers and re-exports.
packages/plugins/context-navigation/src/utils/url/strip-context-query-param.ts Utility to remove $contextId query param when using path/custom encodings.
packages/plugins/context-navigation/src/utils/url/query-utils.ts Query param read/write helpers for $contextId.
packages/plugins/context-navigation/src/utils/url/path-utils.ts Path parsing/writing utilities for /apps/:appKey/:contextId/....
packages/plugins/context-navigation/src/utils/url/index.ts Public URL utilities barrel export.
packages/plugins/context-navigation/src/utils/url/app-route.ts URLPattern-based parsing/building of /apps/:appKey/:contextId?/….
packages/plugins/context-navigation/src/utils/resolve-context-from-url.ts Default initial context resolver using adapter decode.
packages/plugins/context-navigation/src/utils/legacy-app-navigation-fix.ts Optional fix for legacy navigation module (< v7) apps.
packages/plugins/context-navigation/src/utils/has-custom-context-generators.ts Detects apps with custom context URL hooks (custom adapter selection).
packages/plugins/context-navigation/src/types.ts Plugin public types (adapters, config, events, sources).
packages/plugins/context-navigation/src/sources/index.ts Barrel export for built-in source factories.
packages/plugins/context-navigation/src/sources/context-first-source.ts Context-first reconciler source strategy.
packages/plugins/context-navigation/src/sources/app-first-source.ts App-first reconciler source strategy (default).
packages/plugins/context-navigation/src/plugin.ts Core runtime plugin: reconciler + URL guard + event dispatch.
packages/plugins/context-navigation/src/index.ts Package public API surface exports + side-effect event augmentation.
packages/plugins/context-navigation/src/events.ts Event map augmentation for context-navigation events.
packages/plugins/context-navigation/src/enable.ts enableContextNavigation helper registering the plugin via configurator.
packages/plugins/context-navigation/src/enable.test.ts Unit tests covering enableContextNavigation registration/teardown wiring.
packages/plugins/context-navigation/src/configurator.ts Fluent-like builder for plugin config defaults + adapter registration.
packages/plugins/context-navigation/src/adapters/query-adapter.ts Query adapter implementation (?$contextId=).
packages/plugins/context-navigation/src/adapters/path-adapter.ts Path adapter implementation (/apps/:appKey/:contextId/...).
packages/plugins/context-navigation/src/adapters/index.ts Barrel export for built-in adapters.
packages/plugins/context-navigation/src/adapters/custom-adapter.ts Custom adapter factory bridging app-owned URL hooks.
packages/plugins/context-navigation/README.md Consumer-facing documentation for the new plugin package.
packages/plugins/context-navigation/package.json New published package manifest + exports map.
packages/modules/navigation/src/NavigationProvider.ts Basename boundary check + trailing-slash behavior adjustments.
packages/modules/context/src/utils/resolve-initial-context.ts Removes URL-based initial context resolution; parent-only resolution.
packages/modules/context/src/utils/resolve-context-from-path.ts Improves extractor docs; strips query/hash; deprecates path resolution helpers.
packages/modules/context/src/types.ts Adds ContextRoutingStrategy type.
packages/modules/context/src/ContextProvider.ts Exposes routingStrategy + adds version typing; sets provider routingStrategy.
packages/modules/context/src/ContextConfigBuilder.ts Adds setRoutingStrategy builder method.
packages/modules/context/src/configurator.ts Adds routingStrategy and extends generator signature; defaults strategy + warns.
packages/modules/context/README.md Documents routing strategy builder usage.
packages/modules/context/package.json Adds semver/@types/semver deps/peers.
packages/modules/context/docs/migration-routing-strategy.md New migration guide for routing strategy adoption.
packages/dev-portal/src/useAppContextNavigation.ts Removes old ad-hoc context→URL sync hook.
packages/dev-portal/src/Router.tsx Removes usage of deleted hook; updates router documentation.
packages/dev-portal/src/config.ts Enables context module + new context-navigation plugin; exposes modules on window.
packages/dev-portal/src/config-context.ts Adds dev-portal context URL hook wiring via plugin URL utils.
packages/dev-portal/README.md Updates dev-portal docs to reference plugin-based context navigation.
packages/dev-portal/package.json Adds plugin dependency + semver deps to satisfy updated module typings.
cookbooks/portal-analytics/src/framworkConfig.ts Sets explicit routing strategy for cookbook portal context module.
cookbooks/app-react-context/src/config.ts Sets explicit routing strategy for cookbook app.
cookbooks/app-react-context-custom-error/src/config.ts Sets explicit routing strategy for cookbook app.
cookbooks/app-react-bookmark/src/config.ts Sets explicit routing strategy for cookbook app.
cookbooks/app-react-bookmark-advanced/src/config.ts Sets explicit routing strategy for cookbook app.
.changeset/fusion-framework-plugin-context-navigation_initial.md Changeset for initial plugin release.
.changeset/fusion-framework-module-navigation_trailing-slash-fix.md Changeset describing navigation basename/trailing-slash fixes.
.changeset/fusion-framework-module-context_routing-strategy.md Changeset describing context routing strategy changes + deprecations.
.changeset/fusion-framework-dev-portal_context-navigation-handler.md Changeset describing dev-portal integration update.

Comment thread packages/plugins/context-navigation/src/utils/url/path-utils.ts Outdated
Comment thread packages/plugins/context-navigation/src/utils/url/path-utils.ts Outdated
Comment thread packages/dev-portal/src/config.ts Outdated
Comment thread packages/modules/context/docs/migration-routing-strategy.md Outdated
Comment thread .changeset/fusion-framework-module-context_routing-strategy.md Outdated
Comment thread vue-press/src/plugins/README.md
Comment thread packages/modules/context/docs/migration-routing-strategy.md Outdated
Comment thread packages/plugins/context-navigation/src/adapters/path-adapter.ts Outdated
@odinr
odinr force-pushed the feat/framework-module-configurator-refactor branch from 8e82081 to 3c6b04d Compare June 1, 2026 09:25
Base automatically changed from feat/framework-module-configurator-refactor to main June 1, 2026 09:28
@Noggling
Noggling force-pushed the feat/context-navigation-plugin branch 2 times, most recently from 8b94fef to b34a593 Compare June 4, 2026 06:16
Comment thread packages/dev-portal/src/config.ts Outdated
Comment thread packages/modules/context/src/configurator.ts Outdated
Comment thread packages/plugins/context-navigation/src/utils/url/path-utils.ts Outdated
@odinr

odinr commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

splitRelativePath in path-utils.ts is a hand-rolled duplicate of resolvePath from @equinor/fusion-framework-module-navigation. Same story for the manual path reassembly at the end of writeContextIdToQueryParam in query-utils.ts — that's pathToString from the same module. Both already exist, they're just not exported yet.

Two-part ask: expose them from the navigation module, then delete the duplicates and use the real ones here.

Step 1 — add a ./utils sub-path export to the navigation module.

In packages/modules/navigation/package.json:

"./utils": {
  "import": "./dist/esm/lib/utils/index.js",
  "types": "./dist/types/lib/utils/index.d.ts"
}

In packages/modules/navigation/src/lib/utils/index.ts, ensure resolvePath, pathToString, and pathToUrl are all exported (they already exist, just verify the barrel includes them).

Step 2 — replace splitRelativePath in path-utils.ts:

- import { parseAppRoute, buildAppRoute } from './app-route';
+ import { resolvePath } from '@equinor/fusion-framework-module-navigation/utils';
+ import { parseAppRoute, buildAppRoute } from './app-route';

- export const splitRelativePath = (path: string) => { ... };

  export const readContextIdFromAppPath = (path: string) => {
-   const { pathname } = splitRelativePath(path);
+   const { pathname } = resolvePath(path);
    return parseAppRoute(pathname)?.contextId;
  };

Step 3 — replace the manual reassembly in writeContextIdToQueryParam in query-utils.ts:

- import { splitRelativePath } from './path-utils';
+ import { resolvePath, pathToString } from '@equinor/fusion-framework-module-navigation/utils';

  export const writeContextIdToQueryParam = (path: string, contextId?: string): string => {
-   const { pathname, search, hash } = splitRelativePath(path);
+   const { pathname, search, hash } = resolvePath(path);
    ...
-   return `${pathname}${nextSearch ? `?${nextSearch}` : ''}${hash ? `#${hash}` : ''}`;
+   return pathToString({ pathname, search: nextSearch, hash });
  };

Step 4 — drop splitRelativePath from the ./utils public export in index.ts and url-utils.ts — nothing outside this package should have needed it.

@odinr

odinr commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

createContextNavigationPlugin is one giant closure with two subscriptions, shared mutable state, and branching logic spread across helper functions that all close over the same locals. It works, but it's hard to read in isolation and basically untestable as-is. A few concrete issues:


1. The guard switchMap nest is 4 levels deep with no names.

app.current$.pipe(
  switchMap((currentApp) => {
    if (!currentApp) return EMPTY;
    return currentApp.instance$.pipe(
      switchMap((appModules) => {
        if (!appModules) return EMPTY;
        return navigation.state$.pipe(
          switchMap(() => of({ appModules, appKey: currentApp.appKey })),
        );
      }),
    );
  }),
);

Each level is a separate concern (app switch, module resolution, nav event). Extract this into a named operator or factory function so the guard's subscribe body reads as what it does, not how it plumbs streams together:

// operators/active-app-navigation-events.ts
export function activeAppNavigationEvents$(
  app: AppModuleProvider,
  navigation: INavigationProvider,
): Observable<{ appKey: string; appModules: AppModulesInstance<[ContextModule]> }> {
  return app.current$.pipe(
    switchMap((currentApp) => (!currentApp ? EMPTY : currentApp.instance$.pipe(
      switchMap((appModules) => (!appModules ? EMPTY : navigation.state$.pipe(
        map(() => ({ appKey: currentApp.appKey, appModules: appModules as AppModulesInstance<[ContextModule]> })),
      )),
    ))),
  );
}

Then the wire-up becomes readable:

subscriptions.add(
  activeAppNavigationEvents$(app, navigation).subscribe(handleGuardTick),
);

2. The guard's subscribe callback does 6 sequential things with no names.

URL scope check → own-nav token → adapter resolution → URL decode → push-mode branch → replace-mode fallback. Every branch is an implicit return in an imperative cascade. This is the exact shape that should be broken into named functions:

function isInAppScope(currentURL: URL, appKey: string): boolean { ... }
function consumeOwnNavToken(currentURL: URL): boolean { ... }   // returns true if this was our own nav
function handlePushModeGuard(...): void { ... }
function handleReplaceModeGuard(...): void { ... }

Then the callback reads like a decision tree, not archaeology.


3. lastNavigatedPath is shared mutable state bridging two independent subscriptions.

applyNavigation writes it; the guard reads and clears it exactly once. This works right now because there are exactly two subscribers. But:

  • If applyNavigation fires concurrently (two rapid context changes), the second write overwrites the first before the guard sees it — the guard will skip the first navigation's URL forever.
  • If you ever add a third caller of applyNavigation, the invariant breaks with no compile error.

Better: make it a Set<string> (handles concurrent writes), or better still, pass own-nav tokens through the stream itself so the guard observable can filter them without touching closure state at all.


4. applyNavigation is fire-and-forget async inside a sync subscribe.

The .then() happens after subscribe returns. That means navigation errors from the event dispatch are silently swallowed if .catch() ever trips (it's not called here). At minimum pipe the rejection: .then(...).catch((err) => log(...)). Better: make applyNavigation return the promise so callers can decide what to do with it.


5. Nothing in this file is independently testable.

Every function closes over config, event, navigation, log, and lastNavigatedPath. There are no unit tests for the guard or reconciler logic (Copilot already flagged this in an earlier review thread). Extracting named operators and pure helper functions would make it possible to test the push/replace branching, the own-nav token logic, and the adapter dispatch path without standing up the full plugin.

Comment thread packages/plugins/context-navigation/src/__tests__/plugin.test.ts Fixed
@Noggling
Noggling force-pushed the feat/context-navigation-plugin branch from 45b8216 to 1ff2b44 Compare June 5, 2026 08:20
@github-actions github-actions Bot removed 🐞 bug Something isn't working 🚧 chore maintaines work, (update deps, workflos ...) labels Jun 5, 2026
@Noggling
Noggling requested a review from Copilot June 5, 2026 08:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 67 out of 68 changed files in this pull request and generated 9 comments.

Comment thread packages/plugins/context-navigation/src/plugin.ts Outdated
Comment thread packages/modules/context/src/configurator.ts Outdated
Comment thread packages/modules/context/src/ContextProvider.ts
Comment thread packages/plugins/context-navigation/README.md
Comment thread vue-press/src/plugins/README.md
Comment thread cookbooks/app-react-context/src/config.ts
Comment thread packages/modules/navigation/src/NavigationProvider.ts Outdated
Comment thread .changeset/fusion-framework-plugin-context-navigation_initial.md
@Noggling
Noggling force-pushed the feat/context-navigation-plugin branch from 1ff2b44 to c28b79a Compare June 5, 2026 08:38
Noggling added a commit that referenced this pull request Aug 3, 2026
Hash fragments are intentionally cleared when context changes to prevent
inconsistent app state. When a context change resets the app to its root
view, preserving the hash would leave anchors pointing to sections that
may not exist in the new context.

This matches the existing behavior for sub-routes (intentionally dropped)
and maintains consistency across all three adapters (path, query, custom).

Adds test to verify hash is cleared on context change.

Addresses PR review feedback in #4751
@Noggling
Noggling force-pushed the feat/context-navigation-plugin branch 6 times, most recently from 5b5725b to 9624158 Compare August 3, 2026 12:22
@github-actions github-actions Bot added the 🚧 chore maintaines work, (update deps, workflos ...) label Aug 3, 2026
@Noggling
Noggling force-pushed the feat/context-navigation-plugin branch from 9624158 to 516a76c Compare August 3, 2026 12:23
eikeland
eikeland previously approved these changes Aug 4, 2026
Noggling added 15 commits August 5, 2026 11:51
Introduce a new plugin that manages bidirectional synchronization
between the browser URL and the selected context (e.g. facility,
project).

Supports three routing strategies:
- 'path': context ID embedded in the URL path segment
- 'query': context ID as a query parameter (?contextId=...)
- 'custom': consumer-provided adapter for non-standard URL layouts

Key capabilities:
- URL guard that intercepts navigation to resolve context from URLs
- Reconciler that updates the URL when context changes programmatically
- App-first and context-first source factories for initial resolution
- Active-app navigation event stream for cross-app context handoff
- Legacy app path normalization for backward compatibility

Includes full test coverage for plugin lifecycle, guard handlers,
reconciliation logic, and navigation event operators.
Extend the context module with a routingStrategy configuration option
that controls how context identity is represented in URLs ('path' or
'query'). This is the foundation the context-navigation plugin reads
to decide which adapter to use.

Context module changes:
- Add routingStrategy to ContextModuleConfig and configurator
- Add setRoutingStrategy() to ContextConfigBuilder
- Update resolveContextFromPath with custom matcher support and TSDoc
- Simplify resolveInitialContext to only resolve from parent context
  (URL-based resolution is now handled by the plugin)
- Expose routingStrategy on ContextProvider and IContextProvider
- Add routingStrategy as third argument to generatePathFromContext
- Add migration guide for routing strategy adoption

Navigation module changes:
- Stop stripping trailing slashes in normalizePathname
- Fix basename boundary check in _isWithinBasenameScope to prevent
  false positives from overlapping app name prefixes
- Fall back to '/' in _localizePath when basename-stripped path is empty
Wire up the context-navigation plugin in the dev portal so that apps
running in the development shell get automatic URL-to-context
synchronization matching production behavior.

Dev portal changes:
- Enable context-navigation plugin with setPortalName, setDebug,
  and setUrlGuard in dev portal config
- Enable context module with path generator and extractor using
  plugin utility functions (buildContextUrlForStrategy,
  resolveContextIdFromUrl)
- Set explicit 'path' routing strategy
- Delete useAppContextNavigation hook (replaced by plugin)
- Remove useAppContextNavigation() call from Router component
- Update README to document context navigation plugin integration

Cookbook changes:
- Add explicit setRoutingStrategy('path') to context-using cookbook
  configs (app-react-context, app-react-bookmark,
  app-react-bookmark-advanced, app-react-context-custom-error,
  portal-analytics)
VuePress documentation:
- Add top-level plugins section with overview of plugin vs module
  intent and the context-navigation plugin README
- Add context routing strategy migration guide under modules/context
- Update sidebar and theme config for new plugin section

Changesets:
- plugin-context-navigation: initial release (minor)
- module-context: routing strategy addition (minor)
- module-navigation: trailing-slash and basename fix (patch)
- dev-portal: context navigation integration (patch)

Also updates pnpm-lock.yaml for new plugin package dependency.
…anges vs main

The previous changeset described removing routingStrategy from the context
module, but that feature never existed on main. The actual change is moving
URL-based initial context resolution from the context module to the
context-navigation plugin.

Changes:
- Downgrade from major to patch (no breaking changes vs main)
- Accurately describe the URL resolution responsibility shift
- Remove references to routingStrategy removal (never existed on main)
- Keep migration guidance focused on plugin enablement
…ft correction to prevent back-navigation traps
Replace regex-based slash collapsing with iterative approach to eliminate
potential Regular Expression Denial of Service attack when processing
user-controlled basename values with pathological input.

The normalizePathname function now guarantees O(n) linear time complexity
instead of using /\/+/g regex pattern that could be exploited with
thousands of consecutive slashes.

Adds ReDoS protection test to verify performance with pathological input.

Resolves CodeQL security alert: Polynomial regular expression used on
uncontrolled data (High severity)

Related: #4751
Hash fragments are intentionally cleared when context changes to prevent
inconsistent app state. When a context change resets the app to its root
view, preserving the hash would leave anchors pointing to sections that
may not exist in the new context.

This matches the existing behavior for sub-routes (intentionally dropped)
and maintains consistency across all three adapters (path, query, custom).

Adds test to verify hash is cleared on context change.

Addresses PR review feedback in #4751
…slash removal

Replace regex-based trailing slash removal (.replace(/\/+$/, '')) with
iterative stripTrailingSlashes() function to prevent ReDoS attacks.

The /\/+$/ pattern can cause exponential backtracking when processing
basenames with thousands of trailing slashes. The new stripTrailingSlashes
function guarantees O(n) linear time by scanning backwards from the end.

Added test verifying performance with 10k trailing slashes (< 100ms).

This completes the ReDoS vulnerability remediation - both consecutive
slash patterns are now safe:
- normalizePathname: O(n) forward scan (commit ee14d80)
- stripTrailingSlashes: O(n) backward scan (this commit)

Addresses CodeQL alert: Polynomial regular expression used on uncontrolled data
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 65.75% 3337 / 5075
🔵 Statements 65.15% 3968 / 6090
🔵 Functions 51.54% 1134 / 2200
🔵 Branches 55.57% 1845 / 3320
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/modules/context/src/ContextProvider.ts 0% 0% 0% 0% 349-1049
packages/modules/context/src/configurator.ts 0% 0% 0% 0% 194-291
packages/modules/context/src/utils/resolve-initial-context.ts 25% 0% 0% 25% 13-19, 34-37
packages/modules/navigation/src/NavigationProvider.ts 86.84% 77.55% 77.27% 86.3% 133-139, 246-256, 274-317
packages/plugins/context-navigation/src/ContextNavigationConfigurator.ts 34.61% 75% 25% 36% 60-97, 128-168
packages/plugins/context-navigation/src/apply-navigation.ts 100% 100% 100% 100%
packages/plugins/context-navigation/src/create-context-navigation-plugin.ts 86.11% 70% 100% 85.71% 89, 137, 151, 163, 171
packages/plugins/context-navigation/src/enable-context-navigation.ts 100% 100% 100% 100%
packages/plugins/context-navigation/src/reconcile.ts 96.77% 94.11% 100% 96.77% 89
packages/plugins/context-navigation/src/adapters/create-path-adapter.ts 19.04% 12.5% 50% 20% 46-84, 96
packages/plugins/context-navigation/src/adapters/create-query-adapter.ts 20% 60% 50% 20% 47-79
packages/plugins/context-navigation/src/adapters/custom-adapter/create-custom-adapter.ts 3.22% 0% 20% 3.22% 38-131
packages/plugins/context-navigation/src/adapters/custom-adapter/normalize-string-result.ts 0% 0% 0% 0% 22-37
packages/plugins/context-navigation/src/adapters/custom-adapter/to-app-relative.ts 0% 0% 0% 0% 26-32
packages/plugins/context-navigation/src/adapters/custom-adapter/to-full-path.ts 0% 0% 0% 0% 23-26
packages/plugins/context-navigation/src/constants/context-query-param-key.ts 100% 100% 100% 100%
packages/plugins/context-navigation/src/constants/index.ts 100% 100% 100% 100%
packages/plugins/context-navigation/src/constants/uuid-pattern.ts 100% 100% 100% 100%
packages/plugins/context-navigation/src/guard-handlers/consume-own-nav-token.ts 100% 100% 100% 100%
packages/plugins/context-navigation/src/guard-handlers/handle-push-mode-guard.ts 81.81% 50% 100% 81.81% 41, 51
packages/plugins/context-navigation/src/guard-handlers/handle-replace-mode-guard.ts 80% 50% 100% 80% 35, 45
packages/plugins/context-navigation/src/guard-handlers/index.ts 100% 100% 100% 100%
packages/plugins/context-navigation/src/guard-handlers/is-in-app-scope.ts 100% 100% 100% 100%
packages/plugins/context-navigation/src/helpers/get-current-url.ts 100% 50% 100% 100%
packages/plugins/context-navigation/src/helpers/index.ts 100% 100% 100% 100%
packages/plugins/context-navigation/src/helpers/normalize-path-from-url.ts 100% 100% 100% 100%
packages/plugins/context-navigation/src/helpers/normalize-path.ts 75% 75% 100% 75% 19, 29
packages/plugins/context-navigation/src/helpers/resolve-adapter.ts 100% 100% 100% 100%
packages/plugins/context-navigation/src/helpers/strip-query-params.ts 100% 100% 100% 100%
packages/plugins/context-navigation/src/operators/active-app-navigation-events.ts 100% 100% 100% 100%
packages/plugins/context-navigation/src/sources/app-first-source.ts 30% 0% 40% 30% 27-55
packages/plugins/context-navigation/src/sources/types.ts 25% 0% 0% 33.33% 87-88
packages/plugins/context-navigation/src/utils/has-custom-context-generators.ts 0% 0% 0% 0% 14
packages/plugins/context-navigation/src/utils/resolve-context-from-url.ts 69.23% 62.5% 100% 69.23% 43-53
packages/plugins/context-navigation/src/utils/url/build-app-route.ts 14.28% 0% 0% 20% 11-19
packages/plugins/context-navigation/src/utils/url/parse-app-route.ts 58.33% 41.66% 100% 58.33% 38, 49-59
packages/plugins/context-navigation/src/utils/url/strip-context-query-param.ts 0% 0% 0% 0% 15-18
Generated in workflow #15114 for commit 477481f by the Vitest Coverage Report Action

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐞 bug Something isn't working 🚧 chore maintaines work, (update deps, workflos ...) 📚 documentation Improvements or additions to documentation 🚀 feature New feature or request 🧬 Modules

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants