diff --git a/node_modules/.bin/baseline-browser-mapping b/node_modules/.bin/baseline-browser-mapping
index bd699d95..66dec50e 100755
--- a/node_modules/.bin/baseline-browser-mapping
+++ b/node_modules/.bin/baseline-browser-mapping
@@ -10,9 +10,9 @@ case `uname` in
esac
if [ -z "$NODE_PATH" ]; then
- export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/baseline-browser-mapping/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules"
+ export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/baseline-browser-mapping/dist/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/baseline-browser-mapping/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules"
else
- export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/baseline-browser-mapping/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH"
+ export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/baseline-browser-mapping/dist/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/baseline-browser-mapping/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../baseline-browser-mapping/dist/cli.cjs" "$@"
diff --git a/node_modules/.bin/browserslist b/node_modules/.bin/browserslist
index f04a911a..680fc65f 100755
--- a/node_modules/.bin/browserslist
+++ b/node_modules/.bin/browserslist
@@ -10,12 +10,12 @@ case `uname` in
esac
if [ -z "$NODE_PATH" ]; then
- export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/browserslist/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules"
+ export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/browserslist@4.28.2/node_modules/browserslist/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/browserslist@4.28.2/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules"
else
- export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/browserslist/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH"
+ export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/browserslist@4.28.2/node_modules/browserslist/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/browserslist@4.28.2/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
- exec "$basedir/node" "$basedir/../browserslist/cli.js" "$@"
+ exec "$basedir/node" "$basedir/../.pnpm/browserslist@4.28.2/node_modules/browserslist/cli.js" "$@"
else
- exec node "$basedir/../browserslist/cli.js" "$@"
+ exec node "$basedir/../.pnpm/browserslist@4.28.2/node_modules/browserslist/cli.js" "$@"
fi
diff --git a/node_modules/.bin/esbuild b/node_modules/.bin/esbuild
index 182e5317..699250af 100755
--- a/node_modules/.bin/esbuild
+++ b/node_modules/.bin/esbuild
@@ -10,9 +10,12 @@ case `uname` in
esac
if [ -z "$NODE_PATH" ]; then
- export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/esbuild/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules"
+ export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/esbuild@0.25.12/node_modules/esbuild/bin/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/esbuild@0.25.12/node_modules/esbuild/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/esbuild@0.25.12/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules"
else
- export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/esbuild/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH"
+ export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/esbuild@0.25.12/node_modules/esbuild/bin/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/esbuild@0.25.12/node_modules/esbuild/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/esbuild@0.25.12/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH"
+fi
+if [ -x "$basedir/node" ]; then
+ exec "$basedir/node" "$basedir/../.pnpm/esbuild@0.25.12/node_modules/esbuild/bin/esbuild" "$@"
+else
+ exec node "$basedir/../.pnpm/esbuild@0.25.12/node_modules/esbuild/bin/esbuild" "$@"
fi
-"$basedir/../esbuild/bin/esbuild" "$@"
-exit $?
diff --git a/node_modules/.bin/jsesc b/node_modules/.bin/jsesc
index e1507058..fd90df35 100755
--- a/node_modules/.bin/jsesc
+++ b/node_modules/.bin/jsesc
@@ -10,9 +10,9 @@ case `uname` in
esac
if [ -z "$NODE_PATH" ]; then
- export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/jsesc/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules"
+ export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/jsesc/bin/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/jsesc/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules"
else
- export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/jsesc/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH"
+ export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/jsesc/bin/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/jsesc/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../jsesc/bin/jsesc" "$@"
diff --git a/node_modules/.bin/json5 b/node_modules/.bin/json5
index 31f748b2..e9618bb0 100755
--- a/node_modules/.bin/json5
+++ b/node_modules/.bin/json5
@@ -10,9 +10,9 @@ case `uname` in
esac
if [ -z "$NODE_PATH" ]; then
- export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/json5/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules"
+ export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/json5/lib/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/json5/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules"
else
- export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/json5/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH"
+ export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/json5/lib/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/json5/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../json5/lib/cli.js" "$@"
diff --git a/node_modules/.bin/loose-envify b/node_modules/.bin/loose-envify
index c8bed453..feeb9083 100755
--- a/node_modules/.bin/loose-envify
+++ b/node_modules/.bin/loose-envify
@@ -10,9 +10,9 @@ case `uname` in
esac
if [ -z "$NODE_PATH" ]; then
- export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/loose-envify/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules"
+ export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/loose-envify/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules"
else
- export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/loose-envify/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH"
+ export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/loose-envify/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../loose-envify/cli.js" "$@"
diff --git a/node_modules/.bin/nanoid b/node_modules/.bin/nanoid
index 7464908c..aef8f1dc 100755
--- a/node_modules/.bin/nanoid
+++ b/node_modules/.bin/nanoid
@@ -10,9 +10,9 @@ case `uname` in
esac
if [ -z "$NODE_PATH" ]; then
- export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/nanoid/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules"
+ export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/nanoid/bin/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/nanoid/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules"
else
- export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/nanoid/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH"
+ export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/nanoid/bin/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/nanoid/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../nanoid/bin/nanoid.cjs" "$@"
diff --git a/node_modules/.bin/parser b/node_modules/.bin/parser
index 2e46fdf1..a470ae69 100755
--- a/node_modules/.bin/parser
+++ b/node_modules/.bin/parser
@@ -10,9 +10,9 @@ case `uname` in
esac
if [ -z "$NODE_PATH" ]; then
- export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/@babel/parser/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules"
+ export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/@babel/parser/bin/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/@babel/parser/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/@babel/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules"
else
- export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/@babel/parser/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH"
+ export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/@babel/parser/bin/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/@babel/parser/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/@babel/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../@babel/parser/bin/babel-parser.js" "$@"
diff --git a/node_modules/.bin/rollup b/node_modules/.bin/rollup
index 2985a6b1..b6c19b67 100755
--- a/node_modules/.bin/rollup
+++ b/node_modules/.bin/rollup
@@ -10,12 +10,12 @@ case `uname` in
esac
if [ -z "$NODE_PATH" ]; then
- export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/rollup/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules"
+ export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/rollup@4.60.2/node_modules/rollup/dist/bin/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/rollup@4.60.2/node_modules/rollup/dist/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/rollup@4.60.2/node_modules/rollup/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/rollup@4.60.2/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules"
else
- export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/rollup/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH"
+ export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/rollup@4.60.2/node_modules/rollup/dist/bin/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/rollup@4.60.2/node_modules/rollup/dist/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/rollup@4.60.2/node_modules/rollup/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/rollup@4.60.2/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
- exec "$basedir/node" "$basedir/../rollup/dist/bin/rollup" "$@"
+ exec "$basedir/node" "$basedir/../.pnpm/rollup@4.60.2/node_modules/rollup/dist/bin/rollup" "$@"
else
- exec node "$basedir/../rollup/dist/bin/rollup" "$@"
+ exec node "$basedir/../.pnpm/rollup@4.60.2/node_modules/rollup/dist/bin/rollup" "$@"
fi
diff --git a/node_modules/.bin/semver b/node_modules/.bin/semver
index 334837a1..61306a75 100755
--- a/node_modules/.bin/semver
+++ b/node_modules/.bin/semver
@@ -10,9 +10,9 @@ case `uname` in
esac
if [ -z "$NODE_PATH" ]; then
- export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/semver/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules"
+ export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/semver/bin/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/semver/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules"
else
- export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/semver/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH"
+ export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/semver/bin/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/semver/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../semver/bin/semver.js" "$@"
diff --git a/node_modules/.bin/sha.js b/node_modules/.bin/sha.js
index 37da978c..1c271f80 100755
--- a/node_modules/.bin/sha.js
+++ b/node_modules/.bin/sha.js
@@ -10,9 +10,9 @@ case `uname` in
esac
if [ -z "$NODE_PATH" ]; then
- export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/sha.js/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules"
+ export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/sha.js/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules"
else
- export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/sha.js/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH"
+ export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/sha.js/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../sha.js/bin.js" "$@"
diff --git a/node_modules/.bin/update-browserslist-db b/node_modules/.bin/update-browserslist-db
index 89fcf8da..d16ee75a 100755
--- a/node_modules/.bin/update-browserslist-db
+++ b/node_modules/.bin/update-browserslist-db
@@ -10,9 +10,9 @@ case `uname` in
esac
if [ -z "$NODE_PATH" ]; then
- export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/update-browserslist-db/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules"
+ export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/update-browserslist-db/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules"
else
- export NODE_PATH="/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/update-browserslist-db/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules:/home/gru/drips/jacktruedream/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH"
+ export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/update-browserslist-db/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/node_modules:/mnt/c/Users/Dell/OneDrive/node_modules:/mnt/c/Users/Dell/node_modules:/mnt/c/Users/node_modules:/mnt/c/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../update-browserslist-db/cli.js" "$@"
diff --git a/node_modules/.bin/vite b/node_modules/.bin/vite
index 6d1e3bea..1bf1adc1 120000
--- a/node_modules/.bin/vite
+++ b/node_modules/.bin/vite
@@ -1 +1,21 @@
-../vite/bin/vite.js
\ No newline at end of file
+#!/bin/sh
+basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
+
+case `uname` in
+ *CYGWIN*|*MINGW*|*MSYS*)
+ if command -v cygpath > /dev/null 2>&1; then
+ basedir=`cygpath -w "$basedir"`
+ fi
+ ;;
+esac
+
+if [ -z "$NODE_PATH" ]; then
+ export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/vite@5.4.21_@types+node@25.6.0/node_modules/vite/bin/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/vite@5.4.21_@types+node@25.6.0/node_modules/vite/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/vite@5.4.21_@types+node@25.6.0/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules"
+else
+ export NODE_PATH="/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/vite@5.4.21_@types+node@25.6.0/node_modules/vite/bin/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/vite@5.4.21_@types+node@25.6.0/node_modules/vite/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/vite@5.4.21_@types+node@25.6.0/node_modules:/mnt/c/Users/Dell/OneDrive/Documents/stellar-dev-dashboard/node_modules/.pnpm/node_modules:$NODE_PATH"
+fi
+if [ -x "$basedir/node" ]; then
+ exec "$basedir/node" "$basedir/../.pnpm/vite@5.4.21_@types+node@25.6.0/node_modules/vite/bin/vite.js" "$@"
+else
+ exec node "$basedir/../.pnpm/vite@5.4.21_@types+node@25.6.0/node_modules/vite/bin/vite.js" "$@"
+fi
diff --git a/package.json b/package.json
index 2fd5499e..8675470d 100644
--- a/package.json
+++ b/package.json
@@ -38,6 +38,7 @@
"@babel/preset-react": "^7.24.7",
"@babel/preset-typescript": "^7.24.7",
"@playwright/test": "^1.59.1",
+ "@storybook/react-vite": "^8.5.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
@@ -51,10 +52,9 @@
"jsdom": "^29.1.1",
"msw": "^2.14.3",
"prettier": "^3.3.3",
+ "storybook": "^8.5.0",
"typescript": "^6.0.3",
"vite": "^5.4.0",
- "vitest": "^4.1.5",
- "@storybook/react-vite": "^8.5.0",
- "storybook": "^8.5.0"
+ "vitest": "^2.1.9"
}
}
\ No newline at end of file
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 21475c4e..eb53477e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -83,10 +83,10 @@ importers:
version: 4.7.0(vite@5.4.21(@types/node@25.6.0))
'@vitest/coverage-v8':
specifier: ^4.1.5
- version: 4.1.5(vitest@4.1.5)
+ version: 4.1.5(vitest@2.1.9)
'@vitest/ui':
specifier: ^4.1.5
- version: 4.1.5(vitest@4.1.5)
+ version: 4.1.5(vitest@2.1.9)
jsdom:
specifier: ^29.1.1
version: 29.1.1
@@ -106,8 +106,8 @@ importers:
specifier: ^5.4.0
version: 5.4.21(@types/node@25.6.0)
vitest:
- specifier: ^4.1.5
- version: 4.1.5(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@29.1.1)(msw@2.14.3(@types/node@25.6.0)(typescript@6.0.3))(vite@5.4.21(@types/node@25.6.0))
+ specifier: ^2.1.9
+ version: 2.1.9(@types/node@25.6.0)(@vitest/ui@4.1.5)(jsdom@29.1.1)(msw@2.14.3(@types/node@25.6.0)(typescript@6.0.3))
packages:
@@ -1202,66 +1202,79 @@ packages:
resolution: {integrity: sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.60.2':
resolution: {integrity: sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==}
cpu: [arm]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.60.2':
resolution: {integrity: sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.60.2':
resolution: {integrity: sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.60.2':
resolution: {integrity: sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==}
cpu: [loong64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.60.2':
resolution: {integrity: sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==}
cpu: [loong64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.60.2':
resolution: {integrity: sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.60.2':
resolution: {integrity: sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==}
cpu: [ppc64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.60.2':
resolution: {integrity: sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.60.2':
resolution: {integrity: sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==}
cpu: [riscv64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.60.2':
resolution: {integrity: sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.60.2':
resolution: {integrity: sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.60.2':
resolution: {integrity: sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-openbsd-x64@4.60.2':
resolution: {integrity: sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==}
@@ -1296,9 +1309,6 @@ packages:
'@sinclair/typebox@0.27.10':
resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==}
- '@standard-schema/spec@1.1.0':
- resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
-
'@stellar/js-xdr@3.1.2':
resolution: {integrity: sha512-VVolPL5goVEIsvuGqDc5uiKxV03lzfWdvYg1KikvwheDmTBO68CKDji3bAZ/kppZrx5iTA8z3Ld5yuytcvhvOQ==}
@@ -1429,9 +1439,6 @@ packages:
'@types/babel__traverse@7.28.0':
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
- '@types/chai@5.2.3':
- resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
-
'@types/d3-array@3.2.2':
resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
@@ -1459,9 +1466,6 @@ packages:
'@types/d3-timer@3.0.2':
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
- '@types/deep-eql@4.0.2':
- resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
-
'@types/doctrine@0.0.9':
resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==}
@@ -1527,37 +1531,43 @@ packages:
'@vitest/browser':
optional: true
- '@vitest/expect@4.1.5':
- resolution: {integrity: sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==}
+ '@vitest/expect@2.1.9':
+ resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==}
- '@vitest/mocker@4.1.5':
- resolution: {integrity: sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==}
+ '@vitest/mocker@2.1.9':
+ resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==}
peerDependencies:
msw: ^2.4.9
- vite: ^6.0.0 || ^7.0.0 || ^8.0.0
+ vite: ^5.0.0
peerDependenciesMeta:
msw:
optional: true
vite:
optional: true
+ '@vitest/pretty-format@2.1.9':
+ resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==}
+
'@vitest/pretty-format@4.1.5':
resolution: {integrity: sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==}
- '@vitest/runner@4.1.5':
- resolution: {integrity: sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==}
+ '@vitest/runner@2.1.9':
+ resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==}
- '@vitest/snapshot@4.1.5':
- resolution: {integrity: sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==}
+ '@vitest/snapshot@2.1.9':
+ resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==}
- '@vitest/spy@4.1.5':
- resolution: {integrity: sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==}
+ '@vitest/spy@2.1.9':
+ resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==}
'@vitest/ui@4.1.5':
resolution: {integrity: sha512-3Z9HNFiV0IF1fk0JPiK+7kE1GcaIPefQQIBYur6PM5yFIq6agys3uqP/0t966e1wXfmjbRCHDe7qW236Xjwnag==}
peerDependencies:
vitest: 4.1.5
+ '@vitest/utils@2.1.9':
+ resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==}
+
'@vitest/utils@4.1.5':
resolution: {integrity: sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==}
@@ -1691,6 +1701,10 @@ packages:
buffer@6.0.3:
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
+ cac@6.7.14:
+ resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
+ engines: {node: '>=8'}
+
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
@@ -1706,14 +1720,18 @@ packages:
caniuse-lite@1.0.30001791:
resolution: {integrity: sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==}
- chai@6.2.2:
- resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
+ chai@5.3.3:
+ resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
engines: {node: '>=18'}
chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
+ check-error@2.1.3:
+ resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
+ engines: {node: '>= 16'}
+
ci-info@3.9.0:
resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==}
engines: {node: '>=8'}
@@ -1831,6 +1849,10 @@ packages:
decimal.js@10.6.0:
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
+ deep-eql@5.0.2:
+ resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
+ engines: {node: '>=6'}
+
define-data-property@1.1.4:
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
engines: {node: '>= 0.4'}
@@ -1892,8 +1914,8 @@ packages:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'}
- es-module-lexer@2.1.0:
- resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==}
+ es-module-lexer@1.7.0:
+ resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
es-object-atoms@1.1.1:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
@@ -2245,6 +2267,9 @@ packages:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
+ loupe@3.2.1:
+ resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
+
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
@@ -2387,9 +2412,16 @@ packages:
path-to-regexp@6.3.0:
resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
+ pathe@1.1.2:
+ resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
+
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
+ pathval@2.0.1:
+ resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
+ engines: {node: '>= 14.16'}
+
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -2663,6 +2695,9 @@ packages:
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
engines: {node: '>= 0.8'}
+ std-env@3.10.0:
+ resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
+
std-env@4.1.0:
resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==}
@@ -2727,18 +2762,29 @@ packages:
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
- tinyexec@1.1.1:
- resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==}
- engines: {node: '>=18'}
+ tinyexec@0.3.2:
+ resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
tinyglobby@0.2.16:
resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
engines: {node: '>=12.0.0'}
+ tinypool@1.1.1:
+ resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+
+ tinyrainbow@1.2.0:
+ resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==}
+ engines: {node: '>=14.0.0'}
+
tinyrainbow@3.1.0:
resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==}
engines: {node: '>=14.0.0'}
+ tinyspy@3.0.2:
+ resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==}
+ engines: {node: '>=14.0.0'}
+
tldts-core@7.0.30:
resolution: {integrity: sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==}
@@ -2846,6 +2892,11 @@ packages:
victory-vendor@36.9.2:
resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==}
+ vite-node@2.1.9:
+ resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+ hasBin: true
+
vite@5.4.21:
resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==}
engines: {node: ^18.0.0 || >=20.0.0}
@@ -2877,39 +2928,23 @@ packages:
terser:
optional: true
- vitest@4.1.5:
- resolution: {integrity: sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==}
- engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
+ vitest@2.1.9:
+ resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==}
+ engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
- '@opentelemetry/api': ^1.9.0
- '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
- '@vitest/browser-playwright': 4.1.5
- '@vitest/browser-preview': 4.1.5
- '@vitest/browser-webdriverio': 4.1.5
- '@vitest/coverage-istanbul': 4.1.5
- '@vitest/coverage-v8': 4.1.5
- '@vitest/ui': 4.1.5
+ '@types/node': ^18.0.0 || >=20.0.0
+ '@vitest/browser': 2.1.9
+ '@vitest/ui': 2.1.9
happy-dom: '*'
jsdom: '*'
- vite: ^6.0.0 || ^7.0.0 || ^8.0.0
peerDependenciesMeta:
'@edge-runtime/vm':
optional: true
- '@opentelemetry/api':
- optional: true
'@types/node':
optional: true
- '@vitest/browser-playwright':
- optional: true
- '@vitest/browser-preview':
- optional: true
- '@vitest/browser-webdriverio':
- optional: true
- '@vitest/coverage-istanbul':
- optional: true
- '@vitest/coverage-v8':
+ '@vitest/browser':
optional: true
'@vitest/ui':
optional: true
@@ -4168,8 +4203,6 @@ snapshots:
'@sinclair/typebox@0.27.10': {}
- '@standard-schema/spec@1.1.0': {}
-
'@stellar/js-xdr@3.1.2': {}
'@stellar/stellar-base@12.1.1':
@@ -4347,11 +4380,6 @@ snapshots:
dependencies:
'@babel/types': 7.29.0
- '@types/chai@5.2.3':
- dependencies:
- '@types/deep-eql': 4.0.2
- assertion-error: 2.0.1
-
'@types/d3-array@3.2.2': {}
'@types/d3-color@3.1.3': {}
@@ -4376,8 +4404,6 @@ snapshots:
'@types/d3-timer@3.0.2': {}
- '@types/deep-eql@4.0.2': {}
-
'@types/doctrine@0.0.9': {}
'@types/estree@1.0.8': {}
@@ -4440,7 +4466,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@vitest/coverage-v8@4.1.5(vitest@4.1.5)':
+ '@vitest/coverage-v8@4.1.5(vitest@2.1.9)':
dependencies:
'@bcoe/v8-coverage': 1.0.2
'@vitest/utils': 4.1.5
@@ -4452,45 +4478,48 @@ snapshots:
obug: 2.1.1
std-env: 4.1.0
tinyrainbow: 3.1.0
- vitest: 4.1.5(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@29.1.1)(msw@2.14.3(@types/node@25.6.0)(typescript@6.0.3))(vite@5.4.21(@types/node@25.6.0))
+ vitest: 2.1.9(@types/node@25.6.0)(@vitest/ui@4.1.5)(jsdom@29.1.1)(msw@2.14.3(@types/node@25.6.0)(typescript@6.0.3))
- '@vitest/expect@4.1.5':
+ '@vitest/expect@2.1.9':
dependencies:
- '@standard-schema/spec': 1.1.0
- '@types/chai': 5.2.3
- '@vitest/spy': 4.1.5
- '@vitest/utils': 4.1.5
- chai: 6.2.2
- tinyrainbow: 3.1.0
+ '@vitest/spy': 2.1.9
+ '@vitest/utils': 2.1.9
+ chai: 5.3.3
+ tinyrainbow: 1.2.0
- '@vitest/mocker@4.1.5(msw@2.14.3(@types/node@25.6.0)(typescript@6.0.3))(vite@5.4.21(@types/node@25.6.0))':
+ '@vitest/mocker@2.1.9(msw@2.14.3(@types/node@25.6.0)(typescript@6.0.3))(vite@5.4.21(@types/node@25.6.0))':
dependencies:
- '@vitest/spy': 4.1.5
+ '@vitest/spy': 2.1.9
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
msw: 2.14.3(@types/node@25.6.0)(typescript@6.0.3)
vite: 5.4.21(@types/node@25.6.0)
+ '@vitest/pretty-format@2.1.9':
+ dependencies:
+ tinyrainbow: 1.2.0
+
'@vitest/pretty-format@4.1.5':
dependencies:
tinyrainbow: 3.1.0
- '@vitest/runner@4.1.5':
+ '@vitest/runner@2.1.9':
dependencies:
- '@vitest/utils': 4.1.5
- pathe: 2.0.3
+ '@vitest/utils': 2.1.9
+ pathe: 1.1.2
- '@vitest/snapshot@4.1.5':
+ '@vitest/snapshot@2.1.9':
dependencies:
- '@vitest/pretty-format': 4.1.5
- '@vitest/utils': 4.1.5
+ '@vitest/pretty-format': 2.1.9
magic-string: 0.30.21
- pathe: 2.0.3
+ pathe: 1.1.2
- '@vitest/spy@4.1.5': {}
+ '@vitest/spy@2.1.9':
+ dependencies:
+ tinyspy: 3.0.2
- '@vitest/ui@4.1.5(vitest@4.1.5)':
+ '@vitest/ui@4.1.5(vitest@2.1.9)':
dependencies:
'@vitest/utils': 4.1.5
fflate: 0.8.2
@@ -4499,7 +4528,13 @@ snapshots:
sirv: 3.0.2
tinyglobby: 0.2.16
tinyrainbow: 3.1.0
- vitest: 4.1.5(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@29.1.1)(msw@2.14.3(@types/node@25.6.0)(typescript@6.0.3))(vite@5.4.21(@types/node@25.6.0))
+ vitest: 2.1.9(@types/node@25.6.0)(@vitest/ui@4.1.5)(jsdom@29.1.1)(msw@2.14.3(@types/node@25.6.0)(typescript@6.0.3))
+
+ '@vitest/utils@2.1.9':
+ dependencies:
+ '@vitest/pretty-format': 2.1.9
+ loupe: 3.2.1
+ tinyrainbow: 1.2.0
'@vitest/utils@4.1.5':
dependencies:
@@ -4632,6 +4667,8 @@ snapshots:
base64-js: 1.5.1
ieee754: 1.2.1
+ cac@6.7.14: {}
+
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
@@ -4651,13 +4688,21 @@ snapshots:
caniuse-lite@1.0.30001791: {}
- chai@6.2.2: {}
+ chai@5.3.3:
+ dependencies:
+ assertion-error: 2.0.1
+ check-error: 2.1.3
+ deep-eql: 5.0.2
+ loupe: 3.2.1
+ pathval: 2.0.1
chalk@4.1.2:
dependencies:
ansi-styles: 4.3.0
supports-color: 7.2.0
+ check-error@2.1.3: {}
+
ci-info@3.9.0: {}
cli-width@4.1.0: {}
@@ -4758,6 +4803,8 @@ snapshots:
decimal.js@10.6.0: {}
+ deep-eql@5.0.2: {}
+
define-data-property@1.1.4:
dependencies:
es-define-property: 1.0.1
@@ -4805,7 +4852,7 @@ snapshots:
es-errors@1.3.0: {}
- es-module-lexer@2.1.0: {}
+ es-module-lexer@1.7.0: {}
es-object-atoms@1.1.1:
dependencies:
@@ -5204,6 +5251,8 @@ snapshots:
dependencies:
js-tokens: 4.0.0
+ loupe@3.2.1: {}
+
lru-cache@10.4.3: {}
lru-cache@11.3.6: {}
@@ -5335,8 +5384,12 @@ snapshots:
path-to-regexp@6.3.0: {}
+ pathe@1.1.2: {}
+
pathe@2.0.3: {}
+ pathval@2.0.1: {}
+
picocolors@1.1.1: {}
picomatch@2.3.2: {}
@@ -5641,6 +5694,8 @@ snapshots:
statuses@2.0.2: {}
+ std-env@3.10.0: {}
+
std-env@4.1.0: {}
storybook@8.6.18(prettier@3.8.3):
@@ -5697,15 +5752,21 @@ snapshots:
tinybench@2.9.0: {}
- tinyexec@1.1.1: {}
+ tinyexec@0.3.2: {}
tinyglobby@0.2.16:
dependencies:
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
+ tinypool@1.1.1: {}
+
+ tinyrainbow@1.2.0: {}
+
tinyrainbow@3.1.0: {}
+ tinyspy@3.0.2: {}
+
tldts-core@7.0.30: {}
tldts@7.0.30:
@@ -5817,6 +5878,24 @@ snapshots:
d3-time: 3.1.0
d3-timer: 3.0.1
+ vite-node@2.1.9(@types/node@25.6.0):
+ dependencies:
+ cac: 6.7.14
+ debug: 4.4.3
+ es-module-lexer: 1.7.0
+ pathe: 1.1.2
+ vite: 5.4.21(@types/node@25.6.0)
+ transitivePeerDependencies:
+ - '@types/node'
+ - less
+ - lightningcss
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+
vite@5.4.21(@types/node@25.6.0):
dependencies:
esbuild: 0.21.5
@@ -5826,35 +5905,42 @@ snapshots:
'@types/node': 25.6.0
fsevents: 2.3.3
- vitest@4.1.5(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@29.1.1)(msw@2.14.3(@types/node@25.6.0)(typescript@6.0.3))(vite@5.4.21(@types/node@25.6.0)):
+ vitest@2.1.9(@types/node@25.6.0)(@vitest/ui@4.1.5)(jsdom@29.1.1)(msw@2.14.3(@types/node@25.6.0)(typescript@6.0.3)):
dependencies:
- '@vitest/expect': 4.1.5
- '@vitest/mocker': 4.1.5(msw@2.14.3(@types/node@25.6.0)(typescript@6.0.3))(vite@5.4.21(@types/node@25.6.0))
- '@vitest/pretty-format': 4.1.5
- '@vitest/runner': 4.1.5
- '@vitest/snapshot': 4.1.5
- '@vitest/spy': 4.1.5
- '@vitest/utils': 4.1.5
- es-module-lexer: 2.1.0
+ '@vitest/expect': 2.1.9
+ '@vitest/mocker': 2.1.9(msw@2.14.3(@types/node@25.6.0)(typescript@6.0.3))(vite@5.4.21(@types/node@25.6.0))
+ '@vitest/pretty-format': 2.1.9
+ '@vitest/runner': 2.1.9
+ '@vitest/snapshot': 2.1.9
+ '@vitest/spy': 2.1.9
+ '@vitest/utils': 2.1.9
+ chai: 5.3.3
+ debug: 4.4.3
expect-type: 1.3.0
magic-string: 0.30.21
- obug: 2.1.1
- pathe: 2.0.3
- picomatch: 4.0.4
- std-env: 4.1.0
+ pathe: 1.1.2
+ std-env: 3.10.0
tinybench: 2.9.0
- tinyexec: 1.1.1
- tinyglobby: 0.2.16
- tinyrainbow: 3.1.0
+ tinyexec: 0.3.2
+ tinypool: 1.1.1
+ tinyrainbow: 1.2.0
vite: 5.4.21(@types/node@25.6.0)
+ vite-node: 2.1.9(@types/node@25.6.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 25.6.0
- '@vitest/coverage-v8': 4.1.5(vitest@4.1.5)
- '@vitest/ui': 4.1.5(vitest@4.1.5)
+ '@vitest/ui': 4.1.5(vitest@2.1.9)
jsdom: 29.1.1
transitivePeerDependencies:
+ - less
+ - lightningcss
- msw
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
void-elements@3.1.0: {}
diff --git a/src/components/dashboard/SecurityAuditPanel.jsx b/src/components/dashboard/SecurityAuditPanel.jsx
new file mode 100644
index 00000000..8b25a975
--- /dev/null
+++ b/src/components/dashboard/SecurityAuditPanel.jsx
@@ -0,0 +1,608 @@
+import React, { useMemo, useState } from 'react';
+import {
+ Shield,
+ AlertTriangle,
+ AlertOctagon,
+ CheckCircle,
+ RefreshCw,
+ Download,
+ Filter,
+ Activity,
+ Clock,
+ User,
+ ChevronDown,
+ ChevronRight,
+ TrendingUp,
+ FileText,
+} from 'lucide-react';
+import Card from './Card.tsx';
+import { useSecurityAnalysis } from '../../hooks/useSecurityAnalysis.js';
+import { getAuditEntries, exportAuditJson, exportAuditCsv } from '../../utils/audit.js';
+
+const SEVERITY_COLORS = {
+ info: 'var(--cyan)',
+ low: 'var(--text-secondary)',
+ medium: 'var(--yellow)',
+ high: 'var(--orange)',
+ critical: 'var(--red)',
+};
+
+const GRADE_COLORS = {
+ A: 'var(--green)',
+ B: 'var(--cyan)',
+ C: 'var(--yellow)',
+ D: 'var(--orange)',
+ F: 'var(--red)',
+};
+
+const SEVERITY_ICONS = {
+ info: CheckCircle,
+ low: Activity,
+ medium: AlertTriangle,
+ high: AlertOctagon,
+ critical: AlertOctagon,
+};
+
+function RiskGauge({ score, grade }) {
+ const color = GRADE_COLORS[grade] || 'var(--text-muted)';
+ const circumference = 2 * Math.PI * 40;
+ const offset = circumference - (score / 100) * circumference;
+
+ return (
+
+
+
+ );
+}
+
+function FindingRow({ finding, expanded, onToggle }) {
+ const Icon = SEVERITY_ICONS[finding.severity] || AlertTriangle;
+ const color = SEVERITY_COLORS[finding.severity] || 'var(--text-muted)';
+
+ return (
+
+
+
+
+ {finding.message}
+
+
+ {finding.severity}
+
+ {expanded ? (
+
+ ) : (
+
+ )}
+
+ {expanded && (
+
+
+ Type: {finding.type.replace(/_/g, ' ')}
+
+ {finding.actor && (
+
+ Actor: {finding.actor}
+
+ )}
+ {finding.timestamp && (
+
+
+ {new Date(finding.timestamp).toLocaleString()}
+
+ )}
+ {finding.count != null && (
+
+ Count: {finding.count}
+
+ )}
+ {finding.expectedMax != null && (
+
+ Expected max: {finding.expectedMax}
+
+ )}
+
+ )}
+
+ );
+}
+
+function StatBadge({ label, value, color }) {
+ return (
+
+
+ {label}
+
+ {value}
+
+
+ );
+}
+
+export default function SecurityAuditPanel() {
+ const [severityFilter, setSeverityFilter] = useState('');
+ const [categoryFilter, setCategoryFilter] = useState('');
+ const [expandedFindings, setExpandedFindings] = useState(new Set());
+ const [showFilters, setShowFilters] = useState(false);
+
+ const filters = useMemo(() => ({
+ ...(severityFilter && { severity: severityFilter }),
+ ...(categoryFilter && { category: categoryFilter }),
+ }), [severityFilter, categoryFilter]);
+
+ const { report, findings, riskScore, isAnalyzing, refresh } = useSecurityAnalysis({
+ filters,
+ analysisIntervalMs: 5000,
+ });
+
+ const toggleFinding = (index) => {
+ setExpandedFindings((prev) => {
+ const next = new Set(prev);
+ if (next.has(index)) {
+ next.delete(index);
+ } else {
+ next.add(index);
+ }
+ return next;
+ });
+ };
+
+ const handleExport = (format) => {
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
+ const entries = getAuditEntries({ ...filters, limit: 1000 });
+ const content = format === 'json' ? exportAuditJson(entries) : exportAuditCsv(entries);
+ const mime = format === 'json' ? 'application/json' : 'text/csv';
+ const blob = new Blob([content], { type: mime });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `security-report-${stamp}.${format}`;
+ a.click();
+ URL.revokeObjectURL(url);
+ };
+
+ const bySeveritySummary = report.summary?.bySeverity || {};
+ const totalFindings = report.summary?.totalFindings || 0;
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+ Security Analysis
+
+
+ AI-powered audit trail anomaly detection
+
+
+
+
+
+
+
+
+
+
+ {/* Filters */}
+ {showFilters && (
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Risk Score + Summary Stats */}
+
+
50}>
+
+
+
+
+
+
+
+
+
+ 0 ? 'var(--orange)' : 'var(--green)'} />
+
+
+
+
+
+
+ {report.timeRange && (
+
+
+ Analyzing events from {new Date(report.timeRange.from).toLocaleString()} to{' '}
+ {new Date(report.timeRange.to).toLocaleString()}
+
+ )}
+
+ {/* Breakdown bar */}
+ {totalFindings > 0 && (
+
+
+ {['critical', 'high', 'medium', 'low', 'info'].map((sev) => {
+ const count = bySeveritySummary[sev] || 0;
+ if (!count) return null;
+ const pct = (count / totalFindings) * 100;
+ return (
+
+ );
+ })}
+
+
+ )}
+
+
+
+
+ {/* Findings List */}
+
+
+
+
+ }
+ >
+
+ {findings.length === 0 ? (
+
+
+ No security findings detected. All audit events appear normal.
+
+ ) : (
+
+ {findings.map((finding, i) => (
+ toggleFinding(i)}
+ />
+ ))}
+
+ )}
+
+
+
+ {/* Recommendations */}
+ {report.recommendations.length > 0 && (
+
+
+ {report.recommendations.map((rec, i) => (
+
+
+
+ {rec}
+
+
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/src/hooks/useSecurityAnalysis.js b/src/hooks/useSecurityAnalysis.js
new file mode 100644
index 00000000..7fabc1d3
--- /dev/null
+++ b/src/hooks/useSecurityAnalysis.js
@@ -0,0 +1,100 @@
+/**
+ * useSecurityAnalysis Hook
+ *
+ * Integrates the security analysis engine with the audit trail system.
+ * Provides real-time analysis that updates as new audit entries arrive.
+ *
+ * @returns {{
+ * report: object,
+ * findings: object[],
+ * riskScore: object,
+ * isAnalyzing: boolean,
+ * refresh: () => void,
+ * filterEntries: (opts) => object[],
+ * }}
+ */
+
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { getAuditEntries, subscribeAudit } from '../utils/audit.js';
+import {
+ generateReport,
+ computeAggregateRisk,
+ DEFAULT_CONFIG,
+ createStreamingEngine,
+} from '../utils/securityAnalysis.js';
+
+/**
+ * @param {object} [opts]
+ * @param {object} [opts.filters] - Audit entry filters (category, severity, actor, etc.)
+ * @param {number} [opts.analysisIntervalMs=5000] - How often to re-analyze
+ * @param {object} [opts.config] - Custom analysis config overrides
+ */
+export function useSecurityAnalysis(opts = {}) {
+ const { filters = {}, analysisIntervalMs = 5000, config: configOverrides = {} } = opts;
+
+ const config = useMemo(
+ () => ({ ...DEFAULT_CONFIG, ...configOverrides }),
+ [JSON.stringify(configOverrides)], // eslint-disable-line react-hooks/exhaustive-deps
+ );
+
+ const engineRef = useRef(createStreamingEngine(config));
+ const [report, setReport] = useState(() => generateReport([], config));
+ const [isAnalyzing, setIsAnalyzing] = useState(false);
+
+ const analyze = useCallback(() => {
+ setIsAnalyzing(true);
+ try {
+ const entries = getAuditEntries({ ...filters, limit: 1000 });
+ const newReport = generateReport(entries, config);
+ setReport(newReport);
+
+ // Also update the streaming engine
+ const engine = engineRef.current;
+ engine.reset();
+ for (const entry of entries) {
+ engine.addEntry(entry);
+ }
+ } finally {
+ setIsAnalyzing(false);
+ }
+ }, [filters, config]);
+
+ // Initial analysis and periodic refresh
+ useEffect(() => {
+ analyze();
+ const interval = setInterval(analyze, analysisIntervalMs);
+ return () => clearInterval(interval);
+ }, [analyze, analysisIntervalMs]);
+
+ // Subscribe to new audit entries for real-time updates
+ useEffect(() => {
+ const unsub = subscribeAudit((entry) => {
+ // Quick filter check before triggering full re-analysis
+ if (filters.category && entry.category !== filters.category) return;
+ if (filters.severity && entry.severity !== filters.severity) return;
+
+ // Debounce: add to engine immediately, but batch re-analysis
+ engineRef.current.addEntry(entry);
+ });
+ return unsub;
+ }, [filters]);
+
+ const findings = useMemo(() => report.findings, [report]);
+ const riskScore = useMemo(() => report.riskScore, [report]);
+
+ const filterEntries = useCallback(
+ (extraFilters) => getAuditEntries({ ...filters, ...extraFilters }),
+ [filters],
+ );
+
+ return {
+ report,
+ findings,
+ riskScore,
+ isAnalyzing,
+ refresh: analyze,
+ filterEntries,
+ };
+}
+
+export default useSecurityAnalysis;
diff --git a/src/utils/securityAnalysis.js b/src/utils/securityAnalysis.js
new file mode 100644
index 00000000..f2bb876a
--- /dev/null
+++ b/src/utils/securityAnalysis.js
@@ -0,0 +1,812 @@
+/**
+ * Security Audit Trail Analysis Engine
+ *
+ * Provides AI-driven analysis of audit log data to identify:
+ * - Statistical anomalies (unusual event rates, off-hours activity)
+ * - Behavioral patterns (escalation chains, session drift, burst attacks)
+ * - Risk scoring with weighted severity classification
+ * - Actionable security reports with recommendations
+ *
+ * Design goals:
+ * - Pure functions operating on audit entry arrays (no side effects)
+ * - O(n) or O(n log n) complexity for real-time analysis
+ * - Thresholds and weights configurable per deployment
+ */
+
+import { AuditCategory, AuditSeverity } from './audit.js';
+
+// ─── Configuration ────────────────────────────────────────────────────────────
+
+export const DEFAULT_CONFIG = Object.freeze({
+ // Anomaly detection thresholds
+ rateStdDevMultiplier: 2.0,
+ burstWindowMs: 60_000,
+ burstThreshold: 10,
+ offHoursStart: 22,
+ offHoursEnd: 6,
+ minEntriesForBaselines: 20,
+
+ // Pattern detection
+ escalationChainLength: 3,
+ sessionDriftThresholdMs: 24 * 60 * 60 * 1000,
+ maxSessionGapMs: 30 * 60 * 1000,
+
+ // Risk scoring weights
+ weights: {
+ severity: { info: 0, low: 5, medium: 15, high: 35, critical: 60 },
+ category: {
+ auth: 1.2,
+ wallet: 1.1,
+ transaction: 1.3,
+ contract: 1.0,
+ network: 0.6,
+ config: 0.8,
+ data_access: 1.1,
+ export: 0.9,
+ security: 1.5,
+ admin: 1.4,
+ system: 0.3,
+ },
+ outcome: { success: 0, failure: 8, denied: 12 },
+ },
+
+ // Composite score normalization
+ maxRiskScore: 100,
+});
+
+// ─── Severity Numeric Mapping ────────────────────────────────────────────────
+
+const SEVERITY_NUMERIC = {
+ [AuditSeverity.INFO]: 0,
+ [AuditSeverity.LOW]: 1,
+ [AuditSeverity.MEDIUM]: 2,
+ [AuditSeverity.HIGH]: 3,
+ [AuditSeverity.CRITICAL]: 4,
+};
+
+// ─── Helper Utilities ────────────────────────────────────────────────────────
+
+function ts(entry) {
+ return new Date(entry.timestamp).getTime();
+}
+
+function hourOfDay(entry) {
+ return new Date(entry.timestamp).getHours();
+}
+
+function mean(arr) {
+ if (!arr.length) return 0;
+ return arr.reduce((a, b) => a + b, 0) / arr.length;
+}
+
+function stdDev(arr) {
+ if (arr.length < 2) return 0;
+ const m = mean(arr);
+ return Math.sqrt(arr.reduce((s, v) => s + (v - m) ** 2, 0) / (arr.length - 1));
+}
+
+function zScore(value, arr) {
+ const sd = stdDev(arr);
+ if (sd === 0) return value === arr[0] ? 0 : Infinity;
+ return (value - mean(arr)) / sd;
+}
+
+function bucketEntries(entries, bucketMs) {
+ if (!entries.length) return [];
+ const sorted = entries.slice().sort((a, b) => ts(a) - ts(b));
+ const start = ts(sorted[0]);
+ const end = ts(sorted[sorted.length - 1]);
+ const buckets = [];
+ for (let t = start; t <= end; t += bucketMs) {
+ const count = sorted.filter((e) => {
+ const et = ts(e);
+ return et >= t && et < t + bucketMs;
+ }).length;
+ buckets.push({ time: t, count });
+ }
+ return buckets;
+}
+
+// ─── Anomaly Detection ──────────────────────────────────────────────────────
+
+/**
+ * Detect rate-based anomalies using statistical deviation from the mean.
+ * Flags time buckets where event counts exceed the configured threshold.
+ *
+ * @param {object[]} entries - Audit log entries
+ * @param {object} [config]
+ * @returns {{ anomalies: object[], bucketData: object[] }}
+ */
+export function detectRateAnomalies(entries, config = DEFAULT_CONFIG) {
+ if (entries.length < config.minEntriesForBaselines) {
+ return { anomalies: [], bucketData: [] };
+ }
+
+ const bucketData = bucketEntries(entries, config.burstWindowMs);
+ const counts = bucketData.map((b) => b.count);
+ const m = mean(counts);
+ const sd = stdDev(counts);
+ const threshold = m + config.rateStdDevMultiplier * sd;
+
+ const anomalies = bucketData
+ .filter((b) => b.count > threshold && b.count >= config.burstThreshold)
+ .map((b) => ({
+ type: 'rate_spike',
+ severity: b.count > threshold * 2 ? AuditSeverity.CRITICAL : AuditSeverity.HIGH,
+ timestamp: new Date(b.time).toISOString(),
+ count: b.count,
+ expectedMax: Math.round(threshold),
+ deviation: sd > 0 ? ((b.count - m) / sd).toFixed(1) : '∞',
+ message: `Event burst detected: ${b.count} events in window (baseline avg: ${m.toFixed(0)}, threshold: ${Math.round(threshold)})`,
+ }));
+
+ return { anomalies, bucketData };
+}
+
+/**
+ * Detect off-hours activity (configurable quiet period).
+ *
+ * @param {object[]} entries
+ * @param {object} [config]
+ * @returns {object[]}
+ */
+export function detectOffHoursActivity(entries, config = DEFAULT_CONFIG) {
+ const { offHoursStart, offHoursEnd } = config;
+ const offHoursEntries = entries.filter((e) => {
+ const h = hourOfDay(e);
+ return offHoursStart > offHoursEnd
+ ? h >= offHoursStart || h < offHoursEnd
+ : h >= offHoursStart && h < offHoursEnd;
+ });
+
+ return offHoursEntries.map((e) => ({
+ type: 'off_hours_activity',
+ severity: e.severity === AuditSeverity.CRITICAL || e.severity === AuditSeverity.HIGH
+ ? AuditSeverity.HIGH
+ : AuditSeverity.MEDIUM,
+ timestamp: e.timestamp,
+ entryId: e.id,
+ action: e.action,
+ actor: e.actor,
+ message: `Activity at unusual hour (${hourOfDay(e)}:00): ${e.action}`,
+ }));
+}
+
+/**
+ * Detect actor velocity anomalies — an actor performing significantly more
+ * actions than their historical average.
+ *
+ * @param {object[]} entries
+ * @param {object} [config]
+ * @returns {object[]}
+ */
+export function detectActorVelocityAnomalies(entries, config = DEFAULT_CONFIG) {
+ if (entries.length < config.minEntriesForBaselines) {
+ return [];
+ }
+
+ const sorted = entries.slice().sort((a, b) => ts(a) - ts(b));
+ const windowMs = 10 * 60_000;
+ const actorCounts = new Map();
+
+ for (let i = 0; i < sorted.length; i++) {
+ const entry = sorted[i];
+ const actor = entry.actor || 'anonymous';
+ const now = ts(entry);
+
+ if (!actorCounts.has(actor)) {
+ actorCounts.set(actor, []);
+ }
+ const history = actorCounts.get(actor);
+
+ // Prune entries outside the sliding window
+ const windowStart = now - config.sessionDriftThresholdMs;
+ const recent = history.filter((h) => h.time >= windowStart);
+ recent.push(now);
+ actorCounts.set(actor, recent);
+
+ // If we have enough history, check deviation
+ if (recent.length >= config.minEntriesForBaselines) {
+ const intervals = [];
+ for (let j = 1; j < recent.length; j++) {
+ intervals.push(recent[j] - recent[j - 1]);
+ }
+ if (intervals.length >= 3) {
+ const avgInterval = mean(intervals);
+ const currentGap = i > 0 ? now - ts(sorted[i - 1]) : Infinity;
+ if (currentGap < avgInterval * 0.2 && actor !== (sorted[i - 1]?.actor || 'anonymous')) {
+ // Skip — different actors
+ }
+ }
+ }
+ }
+
+ // Aggregate actor totals and detect outliers
+ const actorTotals = new Map();
+ for (const entry of entries) {
+ const actor = entry.actor || 'anonymous';
+ actorTotals.set(actor, (actorTotals.get(actor) || 0) + 1);
+ }
+
+ const totals = [...actorTotals.values()];
+ if (totals.length < 2) return [];
+
+ const m = mean(totals);
+ const sd = stdDev(totals);
+ if (sd === 0) return [];
+ const threshold = m + config.rateStdDevMultiplier * sd;
+
+ const anomalies = [];
+ for (const [actor, count] of actorTotals) {
+ if (count > threshold) {
+ anomalies.push({
+ type: 'actor_velocity',
+ severity: count > threshold * 2 ? AuditSeverity.HIGH : AuditSeverity.MEDIUM,
+ actor,
+ totalActions: count,
+ expectedMax: Math.round(threshold),
+ deviation: ((count - m) / sd).toFixed(1),
+ message: `Actor "${actor}" performed ${count} actions (expected max: ~${Math.round(threshold)})`,
+ });
+ }
+ }
+
+ return anomalies;
+}
+
+/**
+ * Detect unique-IP / unique-actor bursts — many different actors appearing
+ * in a short window (potential coordinated attack or bot activity).
+ *
+ * @param {object[]} entries
+ * @param {object} [config]
+ * @returns {object[]}
+ */
+export function detectMultiActorBursts(entries, config = DEFAULT_CONFIG) {
+ const windowMs = config.burstWindowMs;
+ const sorted = entries.slice().sort((a, b) => ts(a) - ts(b));
+ const anomalies = [];
+
+ for (let i = 0; i < sorted.length; i++) {
+ const windowEnd = ts(sorted[i]);
+ const windowStart = windowEnd - windowMs;
+ const inWindow = sorted.filter((e) => {
+ const t = ts(e);
+ return t >= windowStart && t <= windowEnd;
+ });
+
+ const uniqueActors = new Set(inWindow.map((e) => e.actor || 'anonymous'));
+ if (uniqueActors.size >= config.burstThreshold) {
+ anomalies.push({
+ type: 'multi_actor_burst',
+ severity: AuditSeverity.HIGH,
+ timestamp: sorted[i].timestamp,
+ uniqueActors: uniqueActors.size,
+ totalEvents: inWindow.length,
+ actors: [...uniqueActors].slice(0, 5),
+ message: `${uniqueActors.size} unique actors in ${windowMs / 1000}s window (${inWindow.length} events)`,
+ });
+ // Skip ahead to avoid re-flagging the same window
+ i = sorted.findIndex((e) => ts(e) > windowEnd);
+ if (i === -1) break;
+ }
+ }
+
+ return anomalies;
+}
+
+// ─── Pattern Recognition ────────────────────────────────────────────────────
+
+/**
+ * Detect escalation chains — sequences where severity increases over time
+ * for the same actor or category (e.g., info → medium → critical).
+ *
+ * @param {object[]} entries
+ * @param {object} [config]
+ * @returns {object[]}
+ */
+export function detectEscalationChains(entries, config = DEFAULT_CONFIG) {
+ const sorted = entries.slice().sort((a, b) => ts(a) - ts(b));
+ const minLen = config.escalationChainLength;
+ const chains = [];
+
+ // Group by actor
+ const byActor = new Map();
+ for (const e of sorted) {
+ const actor = e.actor || 'anonymous';
+ if (!byActor.has(actor)) byActor.set(actor, []);
+ byActor.get(actor).push(e);
+ }
+
+ for (const [actor, actorEntries] of byActor) {
+ if (actorEntries.length < minLen) continue;
+
+ let currentChain = [actorEntries[0]];
+ for (let i = 1; i < actorEntries.length; i++) {
+ const prev = SEVERITY_NUMERIC[currentChain[currentChain.length - 1].severity] ?? 0;
+ const curr = SEVERITY_NUMERIC[actorEntries[i].severity] ?? 0;
+
+ if (curr >= prev && ts(actorEntries[i]) - ts(currentChain[currentChain.length - 1]) < config.sessionDriftThresholdMs) {
+ currentChain.push(actorEntries[i]);
+ } else {
+ if (currentChain.length >= minLen) {
+ chains.push({
+ type: 'escalation_chain',
+ severity: AuditSeverity.HIGH,
+ actor,
+ chainLength: currentChain.length,
+ fromSeverity: currentChain[0].severity,
+ toSeverity: currentChain[currentChain.length - 1].severity,
+ startTime: currentChain[0].timestamp,
+ endTime: currentChain[currentChain.length - 1].timestamp,
+ actions: currentChain.map((e) => e.action),
+ message: `Escalation chain: ${actor} went from ${currentChain[0].severity} → ${currentChain[currentChain.length - 1].severity} over ${currentChain.length} events`,
+ });
+ }
+ currentChain = [actorEntries[i]];
+ }
+ }
+ if (currentChain.length >= minLen) {
+ chains.push({
+ type: 'escalation_chain',
+ severity: AuditSeverity.HIGH,
+ actor,
+ chainLength: currentChain.length,
+ fromSeverity: currentChain[0].severity,
+ toSeverity: currentChain[currentChain.length - 1].severity,
+ startTime: currentChain[0].timestamp,
+ endTime: currentChain[currentChain.length - 1].timestamp,
+ actions: currentChain.map((e) => e.action),
+ message: `Escalation chain: ${actor} went from ${currentChain[0].severity} → ${currentChain[currentChain.length - 1].severity} over ${currentChain.length} events`,
+ });
+ }
+ }
+
+ return chains;
+}
+
+/**
+ * Detect repeated-failure patterns — the same action failing multiple times
+ * in succession (may indicate attack, misconfiguration, or stuck process).
+ *
+ * @param {object[]} entries
+ * @param {number} [threshold=3] - Minimum consecutive failures to flag
+ * @returns {object[]}
+ */
+export function detectRepeatedFailures(entries, threshold = 3) {
+ const sorted = entries.slice().sort((a, b) => ts(a) - ts(b));
+ const failures = sorted.filter((e) => e.outcome === 'failure' || e.outcome === 'denied');
+ const patterns = [];
+
+ if (failures.length < threshold) return patterns;
+
+ let streak = [failures[0]];
+ for (let i = 1; i < failures.length; i++) {
+ const gap = ts(failures[i]) - ts(streak[streak.length - 1]);
+ if (failures[i].action === streak[0].action &&
+ failures[i].actor === streak[0].actor &&
+ gap < 10 * 60_000) {
+ streak.push(failures[i]);
+ } else {
+ if (streak.length >= threshold) {
+ patterns.push({
+ type: 'repeated_failures',
+ severity: streak.length >= threshold * 2 ? AuditSeverity.CRITICAL : AuditSeverity.HIGH,
+ action: streak[0].action,
+ actor: streak[0].actor,
+ count: streak.length,
+ startTime: streak[0].timestamp,
+ endTime: streak[streak.length - 1].timestamp,
+ message: `${streak.length} consecutive failures for "${streak[0].action}" by ${streak[0].actor || 'anonymous'}`,
+ });
+ }
+ streak = [failures[i]];
+ }
+ }
+ if (streak.length >= threshold) {
+ patterns.push({
+ type: 'repeated_failures',
+ severity: streak.length >= threshold * 2 ? AuditSeverity.CRITICAL : AuditSeverity.HIGH,
+ action: streak[0].action,
+ actor: streak[0].actor,
+ count: streak.length,
+ startTime: streak[0].timestamp,
+ endTime: streak[streak.length - 1].timestamp,
+ message: `${streak.length} consecutive failures for "${streak[0].action}" by ${streak[0].actor || 'anonymous'}`,
+ });
+ }
+
+ return patterns;
+}
+
+/**
+ * Detect session drift — a session lasting significantly longer than expected,
+ * or entries attributed to the same session with large time gaps.
+ *
+ * @param {object[]} entries
+ * @param {object} [config]
+ * @returns {object[]}
+ */
+export function detectSessionDrift(entries, config = DEFAULT_CONFIG) {
+ const bySession = new Map();
+ for (const e of entries) {
+ const sid = e.sessionId;
+ if (!sid) continue;
+ if (!bySession.has(sid)) bySession.set(sid, []);
+ bySession.get(sid).push(e);
+ }
+
+ const anomalies = [];
+ for (const [sid, sessionEntries] of bySession) {
+ if (sessionEntries.length < 2) continue;
+ const sorted = sessionEntries.slice().sort((a, b) => ts(a) - ts(b));
+ const duration = ts(sorted[sorted.length - 1]) - ts(sorted[0]);
+
+ if (duration > config.sessionDriftThresholdMs) {
+ anomalies.push({
+ type: 'session_drift',
+ severity: AuditSeverity.MEDIUM,
+ sessionId: sid,
+ durationMs: duration,
+ durationHuman: formatDuration(duration),
+ entryCount: sorted.length,
+ startTime: sorted[0].timestamp,
+ endTime: sorted[sorted.length - 1].timestamp,
+ message: `Session ${sid.slice(0, 12)}… lasted ${formatDuration(duration)} (${sorted.length} events)`,
+ });
+ }
+
+ // Check for suspiciously large gaps within a session
+ for (let i = 1; i < sorted.length; i++) {
+ const gap = ts(sorted[i]) - ts(sorted[i - 1]);
+ if (gap > config.maxSessionGapMs && gap < config.sessionDriftThresholdMs) {
+ anomalies.push({
+ type: 'session_gap',
+ severity: AuditSeverity.LOW,
+ sessionId: sid,
+ gapMs: gap,
+ gapHuman: formatDuration(gap),
+ beforeAction: sorted[i - 1].action,
+ afterAction: sorted[i].action,
+ message: `Gap of ${formatDuration(gap)} in session ${sid.slice(0, 12)}… between "${sorted[i - 1].action}" and "${sorted[i].action}"`,
+ });
+ }
+ }
+ }
+
+ return anomalies;
+}
+
+/**
+ * Detect category concentration — an unusually high proportion of events
+ * in a single category (may indicate targeted attack or misconfiguration).
+ *
+ * @param {object[]} entries
+ * @param {number} [concentrationThreshold=0.6] - Min ratio to flag
+ * @returns {object[]}
+ */
+export function detectCategoryConcentration(entries, concentrationThreshold = 0.6) {
+ if (entries.length < 10) return [];
+
+ const byCategory = new Map();
+ for (const e of entries) {
+ byCategory.set(e.category, (byCategory.get(e.category) || 0) + 1);
+ }
+
+ const anomalies = [];
+ for (const [category, count] of byCategory) {
+ const ratio = count / entries.length;
+ if (ratio >= concentrationThreshold) {
+ anomalies.push({
+ type: 'category_concentration',
+ severity: ratio > 0.8 ? AuditSeverity.HIGH : AuditSeverity.MEDIUM,
+ category,
+ count,
+ ratio: ratio.toFixed(2),
+ total: entries.length,
+ message: `${ratio.toFixed(0)}% of events (${count}/${entries.length}) are in category "${category}"`,
+ });
+ }
+ }
+
+ return anomalies;
+}
+
+// ─── Risk Scoring ───────────────────────────────────────────────────────────
+
+/**
+ * Compute a composite risk score for a single audit entry.
+ *
+ * @param {object} entry
+ * @param {object} [config]
+ * @returns {number} 0–100
+ */
+export function scoreEntry(entry, config = DEFAULT_CONFIG) {
+ const { weights, maxRiskScore } = config;
+
+ const severityScore = weights.severity[entry.severity] ?? 0;
+ const categoryMultiplier = weights.category[entry.category] ?? 1.0;
+ const outcomeScore = weights.outcome[entry.outcome] ?? 0;
+
+ const raw = (severityScore + outcomeScore) * categoryMultiplier;
+ return Math.min(maxRiskScore, Math.round(raw));
+}
+
+/**
+ * Compute an aggregate risk score for a set of audit entries.
+ * Uses the maximum individual score weighted by anomaly density.
+ *
+ * @param {object[]} entries
+ * @param {object[]} [anomalies=[]]
+ * @param {object} [config]
+ * @returns {{ score: number, grade: string, breakdown: object }}
+ */
+export function computeAggregateRisk(entries, anomalies = [], config = DEFAULT_CONFIG) {
+ if (!entries.length) {
+ return { score: 0, grade: 'A', breakdown: { severity: 0, anomalies: 0, outcomes: 0 } };
+ }
+
+ const scores = entries.map((e) => scoreEntry(e, config));
+ const maxEntryScore = Math.max(...scores);
+ const avgEntryScore = mean(scores);
+
+ // Anomaly density contribution
+ const anomalyDensity = anomalies.length / Math.max(1, entries.length);
+ const anomalyScore = Math.min(40, anomalyDensity * 100);
+
+ // Failure rate contribution
+ const failures = entries.filter((e) => e.outcome === 'failure' || e.outcome === 'denied').length;
+ const failureRate = failures / entries.length;
+ const outcomeScore = Math.min(30, failureRate * 60);
+
+ // Composite score: weighted blend
+ const composite = Math.min(
+ config.maxRiskScore,
+ Math.round(maxEntryScore * 0.4 + avgEntryScore * 0.2 + anomalyScore * 0.25 + outcomeScore * 0.15),
+ );
+
+ const grade = riskGrade(composite);
+
+ return {
+ score: composite,
+ grade,
+ breakdown: {
+ severity: Math.round(maxEntryScore),
+ anomalies: Math.round(anomalyScore),
+ outcomes: Math.round(outcomeScore),
+ },
+ };
+}
+
+/**
+ * Map a numeric risk score to a letter grade.
+ *
+ * @param {number} score
+ * @returns {string}
+ */
+export function riskGrade(score) {
+ if (score <= 10) return 'A';
+ if (score <= 25) return 'B';
+ if (score <= 50) return 'C';
+ if (score <= 75) return 'D';
+ return 'F';
+}
+
+// ─── Report Generation ──────────────────────────────────────────────────────
+
+/**
+ * Recommendation templates keyed by anomaly/pattern type.
+ */
+const RECOMMENDATIONS = {
+ rate_spike: [
+ 'Investigate the source of the event burst.',
+ 'Consider temporarily rate-limiting affected endpoints.',
+ 'Verify no automated process is malfunctioning.',
+ ],
+ off_hours_activity: [
+ 'Verify the actor was authorized for this time window.',
+ 'Review if off-hours access aligns with on-call schedules.',
+ 'Consider enforcing stricter MFA for off-hours operations.',
+ ],
+ actor_velocity: [
+ 'Review the actor\'s recent activity for legitimacy.',
+ 'Check for compromised credentials or session tokens.',
+ 'Consider imposing per-actor rate limits.',
+ ],
+ multi_actor_burst: [
+ 'Check for coordinated attack or bot activity.',
+ 'Review IP addresses and geolocation of actors.',
+ 'Consider enabling additional CAPTCHA or challenge mechanisms.',
+ ],
+ escalation_chain: [
+ 'Investigate the root cause of the escalating severity.',
+ 'Ensure alerting is configured for critical-severity events.',
+ 'Consider automated containment for rapid escalations.',
+ ],
+ repeated_failures: [
+ 'Check for misconfiguration or service degradation.',
+ 'Review if the actor\'s permissions are correctly assigned.',
+ 'Investigate if this is a brute-force or fuzzing attempt.',
+ ],
+ session_drift: [
+ 'Verify session belongs to a legitimate long-running operation.',
+ 'Check for session fixation or hijacking indicators.',
+ 'Consider implementing session timeout policies.',
+ ],
+ session_gap: [
+ 'Review what happened during the gap period.',
+ 'Check for network interruptions or suspended sessions.',
+ ],
+ category_concentration: [
+ 'Investigate why one category dominates activity.',
+ 'Check for automated scripts or misconfigured integrations.',
+ 'Review access controls for the concentrated category.',
+ ],
+};
+
+/**
+ * Generate a comprehensive security analysis report.
+ *
+ * @param {object[]} entries - Audit log entries to analyze
+ * @param {object} [config]
+ * @returns {object} Full report with findings, risk score, and recommendations
+ */
+export function generateReport(entries, config = DEFAULT_CONFIG) {
+ if (!entries.length) {
+ return {
+ timestamp: new Date().toISOString(),
+ entryCount: 0,
+ riskScore: { score: 0, grade: 'A', breakdown: { severity: 0, anomalies: 0, outcomes: 0 } },
+ findings: [],
+ summary: { totalFindings: 0, byType: {}, bySeverity: {} },
+ recommendations: [],
+ };
+ }
+
+ // Run all detectors
+ const { anomalies: rateAnomalies } = detectRateAnomalies(entries, config);
+ const offHours = detectOffHoursActivity(entries, config);
+ const velocityAnomalies = detectActorVelocityAnomalies(entries, config);
+ const multiActorBursts = detectMultiActorBursts(entries, config);
+ const escalationChains = detectEscalationChains(entries, config);
+ const repeatedFailures = detectRepeatedFailures(entries);
+ const sessionDrift = detectSessionDrift(entries, config);
+ const categoryConcentration = detectCategoryConcentration(entries);
+
+ const allFindings = [
+ ...rateAnomalies,
+ ...offHours,
+ ...velocityAnomalies,
+ ...multiActorBursts,
+ ...escalationChains,
+ ...repeatedFailures,
+ ...sessionDrift,
+ ...categoryConcentration,
+ ].sort((a, b) => {
+ const sa = SEVERITY_NUMERIC[a.severity] ?? 0;
+ const sb = SEVERITY_NUMERIC[b.severity] ?? 0;
+ return sb - sa;
+ });
+
+ // Compute aggregate risk
+ const riskScore = computeAggregateRisk(entries, allFindings, config);
+
+ // Aggregate by type
+ const byType = {};
+ const bySeverity = {};
+ for (const f of allFindings) {
+ byType[f.type] = (byType[f.type] || 0) + 1;
+ bySeverity[f.severity] = (bySeverity[f.severity] || 0) + 1;
+ }
+
+ // Collect unique recommendations
+ const recommendationSet = new Set();
+ for (const f of allFindings) {
+ const recs = RECOMMENDATIONS[f.type] || [];
+ for (const r of recs) recommendationSet.add(r);
+ }
+
+ // Basic entry-level stats
+ const entriesBySeverity = {};
+ const entriesByCategory = {};
+ const entriesByOutcome = {};
+ for (const e of entries) {
+ entriesBySeverity[e.severity] = (entriesBySeverity[e.severity] || 0) + 1;
+ entriesByCategory[e.category] = (entriesByCategory[e.category] || 0) + 1;
+ entriesByOutcome[e.outcome] = (entriesByOutcome[e.outcome] || 0) + 1;
+ }
+
+ return {
+ timestamp: new Date().toISOString(),
+ entryCount: entries.length,
+ timeRange: {
+ from: entries.reduce((min, e) => e.timestamp < min ? e.timestamp : min, entries[0].timestamp),
+ to: entries.reduce((max, e) => e.timestamp > max ? e.timestamp : max, entries[0].timestamp),
+ },
+ riskScore,
+ findings: allFindings,
+ summary: {
+ totalFindings: allFindings.length,
+ byType,
+ bySeverity,
+ },
+ entryStats: {
+ bySeverity: entriesBySeverity,
+ byCategory: entriesByCategory,
+ byOutcome: entriesByOutcome,
+ },
+ recommendations: [...recommendationSet],
+ };
+}
+
+// ─── Real-time Analysis Engine ──────────────────────────────────────────────
+
+/**
+ * Create a streaming analysis engine that maintains rolling windows
+ * and emits findings as new entries arrive.
+ *
+ * @param {object} [config]
+ * @returns {{ addEntry, getFindings, getRiskScore, getReport, reset }}
+ */
+export function createStreamingEngine(config = DEFAULT_CONFIG) {
+ let buffer = [];
+ let cachedFindings = null;
+ let cachedReport = null;
+
+ function addEntry(entry) {
+ buffer.push(entry);
+ // Keep buffer bounded
+ if (buffer.length > 2000) {
+ buffer = buffer.slice(-1500);
+ }
+ // Invalidate caches
+ cachedFindings = null;
+ cachedReport = null;
+ }
+
+ function getFindings() {
+ if (!cachedFindings) {
+ const { anomalies } = detectRateAnomalies(buffer, config);
+ cachedFindings = [
+ ...anomalies,
+ ...detectOffHoursActivity(buffer, config),
+ ...detectActorVelocityAnomalies(buffer, config),
+ ...detectMultiActorBursts(buffer, config),
+ ...detectEscalationChains(buffer, config),
+ ...detectRepeatedFailures(buffer),
+ ...detectSessionDrift(buffer, config),
+ ...detectCategoryConcentration(buffer),
+ ].sort((a, b) => (SEVERITY_NUMERIC[b.severity] ?? 0) - (SEVERITY_NUMERIC[a.severity] ?? 0));
+ }
+ return cachedFindings;
+ }
+
+ function getRiskScore() {
+ return computeAggregateRisk(buffer, getFindings(), config);
+ }
+
+ function getReport() {
+ if (!cachedReport) {
+ cachedReport = generateReport(buffer, config);
+ }
+ return cachedReport;
+ }
+
+ function reset() {
+ buffer = [];
+ cachedFindings = null;
+ cachedReport = null;
+ }
+
+ return { addEntry, getFindings, getRiskScore, getReport, reset, get buffer() { return buffer; } };
+}
+
+// ─── Internal Helpers ───────────────────────────────────────────────────────
+
+function formatDuration(ms) {
+ if (ms < 1000) return `${ms}ms`;
+ if (ms < 60_000) return `${(ms / 1000).toFixed(0)}s`;
+ if (ms < 3600_000) return `${(ms / 60_000).toFixed(0)}m`;
+ if (ms < 86400_000) return `${(ms / 3600_000).toFixed(1)}h`;
+ return `${(ms / 86400_000).toFixed(1)}d`;
+}
diff --git a/tests/unit/utils/securityAnalysis.test.js b/tests/unit/utils/securityAnalysis.test.js
new file mode 100644
index 00000000..89623c4e
--- /dev/null
+++ b/tests/unit/utils/securityAnalysis.test.js
@@ -0,0 +1,758 @@
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+
+vi.mock('../../../src/utils/audit.js', () => ({
+ AuditCategory: {
+ AUTH: 'auth',
+ WALLET: 'wallet',
+ TRANSACTION: 'transaction',
+ CONTRACT: 'contract',
+ NETWORK: 'network',
+ CONFIG: 'config',
+ DATA_ACCESS: 'data_access',
+ EXPORT: 'export',
+ SECURITY: 'security',
+ ADMIN: 'admin',
+ SYSTEM: 'system',
+ },
+ AuditSeverity: {
+ INFO: 'info',
+ LOW: 'low',
+ MEDIUM: 'medium',
+ HIGH: 'high',
+ CRITICAL: 'critical',
+ },
+}));
+
+import {
+ detectRateAnomalies,
+ detectOffHoursActivity,
+ detectActorVelocityAnomalies,
+ detectMultiActorBursts,
+ detectEscalationChains,
+ detectRepeatedFailures,
+ detectSessionDrift,
+ detectCategoryConcentration,
+ scoreEntry,
+ computeAggregateRisk,
+ riskGrade,
+ generateReport,
+ createStreamingEngine,
+ DEFAULT_CONFIG,
+} from '../../../src/utils/securityAnalysis.js';
+
+// ─── Test Factories ─────────────────────────────────────────────────────────
+
+function makeEntry(overrides = {}) {
+ return {
+ id: `test-${Math.random().toString(36).slice(2, 8)}`,
+ timestamp: new Date().toISOString(),
+ action: 'auth.login.success',
+ category: 'auth',
+ severity: 'info',
+ actor: 'user-A',
+ target: null,
+ outcome: 'success',
+ metadata: {},
+ sessionId: 'session-1',
+ hash: 'abc123',
+ prevHash: '0',
+ ...overrides,
+ };
+}
+
+function makeEntries(count, overrides = {}) {
+ const base = makeEntry(overrides);
+ return Array.from({ length: count }, (_, i) => ({
+ ...base,
+ id: `test-${i}`,
+ timestamp: new Date(Date.now() + i * 1000).toISOString(),
+ }));
+}
+
+function makeTimeShiftedEntries(count, intervalMs, actorFn, severityFn, overrides = {}) {
+ const start = new Date('2026-01-01T10:00:00Z').getTime();
+ return Array.from({ length: count }, (_, i) => makeEntry({
+ ...overrides,
+ timestamp: new Date(start + i * intervalMs).toISOString(),
+ actor: typeof actorFn === 'function' ? actorFn(i) : actorFn,
+ severity: typeof severityFn === 'function' ? severityFn(i) : severityFn,
+ }));
+}
+
+// ─── detectRateAnomalies ────────────────────────────────────────────────────
+
+describe('detectRateAnomalies', () => {
+ it('returns empty when below minimum entries threshold', () => {
+ const entries = makeEntries(5);
+ const result = detectRateAnomalies(entries);
+ expect(result.anomalies).toHaveLength(0);
+ expect(result.bucketData).toHaveLength(0);
+ });
+
+ it('returns empty for uniform event distribution', () => {
+ const entries = makeEntries(30);
+ const result = detectRateAnomalies(entries);
+ expect(result.anomalies).toHaveLength(0);
+ expect(result.bucketData.length).toBeGreaterThan(0);
+ });
+
+ it('detects rate spikes when a bucket far exceeds the mean', () => {
+ const start = new Date('2026-01-01T10:00:00Z').getTime();
+ const entries = [];
+ // Spread 25 events across 10 buckets (2-3 per bucket)
+ for (let i = 0; i < 25; i++) {
+ entries.push(makeEntry({
+ timestamp: new Date(start + i * 200_000).toISOString(),
+ }));
+ }
+ // Dump 15 events into one 60s bucket
+ for (let i = 0; i < 15; i++) {
+ entries.push(makeEntry({
+ timestamp: new Date(start + 1_000_000 + i * 1000).toISOString(),
+ }));
+ }
+ const result = detectRateAnomalies(entries);
+ expect(result.anomalies.length).toBeGreaterThanOrEqual(1);
+ expect(result.anomalies[0].type).toBe('rate_spike');
+ expect(result.anomalies[0].count).toBeGreaterThanOrEqual(15);
+ });
+
+ it('uses custom config thresholds', () => {
+ const entries = makeEntries(25);
+ const result = detectRateAnomalies(entries, {
+ ...DEFAULT_CONFIG,
+ minEntriesForBaselines: 5,
+ burstThreshold: 2,
+ });
+ // Even uniform data may not trigger, but should not error
+ expect(result).toHaveProperty('anomalies');
+ expect(result).toHaveProperty('bucketData');
+ });
+});
+
+// ─── detectOffHoursActivity ─────────────────────────────────────────────────
+
+describe('detectOffHoursActivity', () => {
+ it('detects activity during off-hours (22:00-06:00)', () => {
+ const entries = [
+ makeEntry({ timestamp: '2026-01-01T23:00:00Z', action: 'tx.submit', severity: 'info' }),
+ makeEntry({ timestamp: '2026-01-01T03:00:00Z', action: 'wallet.connect', severity: 'info' }),
+ makeEntry({ timestamp: '2026-01-01T12:00:00Z', action: 'auth.login', severity: 'info' }),
+ ];
+ const result = detectOffHoursActivity(entries);
+ expect(result).toHaveLength(2);
+ expect(result[0].type).toBe('off_hours_activity');
+ expect(result[1].type).toBe('off_hours_activity');
+ });
+
+ it('returns empty for all business-hours activity', () => {
+ const entries = [
+ makeEntry({ timestamp: '2026-01-01T09:00:00Z' }),
+ makeEntry({ timestamp: '2026-01-01T14:00:00Z' }),
+ makeEntry({ timestamp: '2026-01-01T17:00:00Z' }),
+ ];
+ const result = detectOffHoursActivity(entries);
+ expect(result).toHaveLength(0);
+ });
+
+ it('returns empty for empty input', () => {
+ expect(detectOffHoursActivity([])).toHaveLength(0);
+ });
+
+ it('grades critical/high severity entries as HIGH in off-hours', () => {
+ const entries = [
+ makeEntry({ timestamp: '2026-01-01T23:30:00Z', severity: 'critical' }),
+ ];
+ const result = detectOffHoursActivity(entries);
+ expect(result[0].severity).toBe('high');
+ });
+
+ it('grades other severity entries as MEDIUM in off-hours', () => {
+ const entries = [
+ makeEntry({ timestamp: '2026-01-01T23:30:00Z', severity: 'low' }),
+ ];
+ const result = detectOffHoursActivity(entries);
+ expect(result[0].severity).toBe('medium');
+ });
+});
+
+// ─── detectActorVelocityAnomalies ───────────────────────────────────────────
+
+describe('detectActorVelocityAnomalies', () => {
+ it('returns empty when below minimum entries', () => {
+ const entries = makeEntries(5);
+ expect(detectActorVelocityAnomalies(entries)).toHaveLength(0);
+ });
+
+ it('returns empty when all actors have similar counts', () => {
+ // Create entries evenly distributed across 5 actors
+ const entries = [];
+ for (let a = 0; a < 5; a++) {
+ for (let i = 0; i < 6; i++) {
+ entries.push(makeEntry({ actor: `actor-${a}` }));
+ }
+ }
+ expect(detectActorVelocityAnomalies(entries)).toHaveLength(0);
+ });
+
+ it('detects an actor with significantly more actions', () => {
+ const entries = [];
+ // Normal actors: 3 actions each
+ for (let a = 0; a < 5; a++) {
+ for (let i = 0; i < 3; i++) {
+ entries.push(makeEntry({ actor: `normal-${a}` }));
+ }
+ }
+ // Overactive actor: 30 actions
+ for (let i = 0; i < 30; i++) {
+ entries.push(makeEntry({ actor: 'overactive' }));
+ }
+ const result = detectActorVelocityAnomalies(entries);
+ expect(result.length).toBeGreaterThanOrEqual(1);
+ const overactive = result.find((r) => r.actor === 'overactive');
+ expect(overactive).toBeDefined();
+ expect(overactive.totalActions).toBe(30);
+ });
+});
+
+// ─── detectMultiActorBursts ─────────────────────────────────────────────────
+
+describe('detectMultiActorBursts', () => {
+ it('detects many unique actors in a short window', () => {
+ const start = Date.now();
+ const entries = [];
+ for (let i = 0; i < 12; i++) {
+ entries.push(makeEntry({
+ actor: `attacker-${i}`,
+ timestamp: new Date(start + i * 1000).toISOString(),
+ }));
+ }
+ const result = detectMultiActorBursts(entries, {
+ ...DEFAULT_CONFIG,
+ burstThreshold: 10,
+ });
+ expect(result.length).toBeGreaterThanOrEqual(1);
+ expect(result[0].type).toBe('multi_actor_burst');
+ expect(result[0].uniqueActors).toBeGreaterThanOrEqual(10);
+ });
+
+ it('returns empty when actors are spread out', () => {
+ const start = new Date('2026-01-01T10:00:00Z').getTime();
+ const entries = [];
+ for (let i = 0; i < 12; i++) {
+ entries.push(makeEntry({
+ actor: `user-${i}`,
+ timestamp: new Date(start + i * 120_000).toISOString(), // 2min apart
+ }));
+ }
+ const result = detectMultiActorBursts(entries);
+ expect(result).toHaveLength(0);
+ });
+
+ it('returns empty for single actor', () => {
+ const entries = makeEntries(15, { actor: 'same-actor' });
+ expect(detectMultiActorBursts(entries)).toHaveLength(0);
+ });
+});
+
+// ─── detectEscalationChains ─────────────────────────────────────────────────
+
+describe('detectEscalationChains', () => {
+ it('detects increasing severity for same actor', () => {
+ const entries = makeTimeShiftedEntries(4, 1000, 'attacker', (i) => {
+ return ['info', 'medium', 'high', 'critical'][i];
+ });
+ const result = detectEscalationChains(entries, {
+ ...DEFAULT_CONFIG,
+ escalationChainLength: 3,
+ });
+ expect(result.length).toBeGreaterThanOrEqual(1);
+ expect(result[0].type).toBe('escalation_chain');
+ expect(result[0].actor).toBe('attacker');
+ expect(result[0].fromSeverity).toBe('info');
+ expect(result[0].toSeverity).toBe('critical');
+ });
+
+ it('returns empty when severity strictly decreases', () => {
+ const entries = makeTimeShiftedEntries(4, 1000, 'user', (i) => {
+ return ['critical', 'high', 'medium', 'info'][i];
+ });
+ const result = detectEscalationChains(entries);
+ expect(result).toHaveLength(0);
+ });
+
+ it('returns empty when chain is shorter than minimum', () => {
+ const entries = makeTimeShiftedEntries(2, 1000, 'user', (i) => {
+ return ['info', 'critical'][i];
+ });
+ const result = detectEscalationChains(entries, {
+ ...DEFAULT_CONFIG,
+ escalationChainLength: 3,
+ });
+ expect(result).toHaveLength(0);
+ });
+
+ it('detects multiple chains for different actors', () => {
+ const start = new Date('2026-01-01T10:00:00Z').getTime();
+ const entries = [
+ makeEntry({ actor: 'a1', severity: 'info', timestamp: new Date(start).toISOString() }),
+ makeEntry({ actor: 'a1', severity: 'medium', timestamp: new Date(start + 1000).toISOString() }),
+ makeEntry({ actor: 'a1', severity: 'critical', timestamp: new Date(start + 2000).toISOString() }),
+ makeEntry({ actor: 'a2', severity: 'low', timestamp: new Date(start).toISOString() }),
+ makeEntry({ actor: 'a2', severity: 'high', timestamp: new Date(start + 1000).toISOString() }),
+ makeEntry({ actor: 'a2', severity: 'critical', timestamp: new Date(start + 2000).toISOString() }),
+ ];
+ const result = detectEscalationChains(entries);
+ expect(result.length).toBeGreaterThanOrEqual(2);
+ });
+});
+
+// ─── detectRepeatedFailures ─────────────────────────────────────────────────
+
+describe('detectRepeatedFailures', () => {
+ it('detects consecutive failures for same action and actor', () => {
+ const start = Date.now();
+ const entries = [];
+ for (let i = 0; i < 5; i++) {
+ entries.push(makeEntry({
+ action: 'tx.submit',
+ actor: 'user-1',
+ outcome: 'failure',
+ timestamp: new Date(start + i * 5000).toISOString(),
+ }));
+ }
+ const result = detectRepeatedFailures(entries);
+ expect(result).toHaveLength(1);
+ expect(result[0].type).toBe('repeated_failures');
+ expect(result[0].count).toBe(5);
+ expect(result[0].action).toBe('tx.submit');
+ });
+
+ it('returns empty when fewer than threshold failures', () => {
+ const entries = [
+ makeEntry({ outcome: 'failure', action: 'auth.login' }),
+ makeEntry({ outcome: 'failure', action: 'auth.login' }),
+ ];
+ expect(detectRepeatedFailures(entries, 3)).toHaveLength(0);
+ });
+
+ it('returns empty when failures are for different actions', () => {
+ const start = Date.now();
+ const entries = [
+ makeEntry({ outcome: 'failure', action: 'auth.login', timestamp: new Date(start).toISOString() }),
+ makeEntry({ outcome: 'failure', action: 'tx.submit', timestamp: new Date(start + 1000).toISOString() }),
+ makeEntry({ outcome: 'failure', action: 'auth.login', timestamp: new Date(start + 2000).toISOString() }),
+ ];
+ expect(detectRepeatedFailures(entries, 3)).toHaveLength(0);
+ });
+
+ it('returns empty when failures are too far apart', () => {
+ const start = new Date('2026-01-01T10:00:00Z').getTime();
+ const entries = [
+ makeEntry({ outcome: 'failure', action: 'auth.login', actor: 'u1', timestamp: new Date(start).toISOString() }),
+ makeEntry({ outcome: 'failure', action: 'auth.login', actor: 'u1', timestamp: new Date(start + 20 * 60_000).toISOString() }),
+ makeEntry({ outcome: 'failure', action: 'auth.login', actor: 'u1', timestamp: new Date(start + 40 * 60_000).toISOString() }),
+ ];
+ expect(detectRepeatedFailures(entries, 3)).toHaveLength(0);
+ });
+
+ it('grades severity as CRITICAL when count >= threshold * 2', () => {
+ const start = Date.now();
+ const entries = [];
+ for (let i = 0; i < 6; i++) {
+ entries.push(makeEntry({
+ outcome: 'denied',
+ action: 'admin.delete',
+ actor: 'admin',
+ timestamp: new Date(start + i * 2000).toISOString(),
+ }));
+ }
+ const result = detectRepeatedFailures(entries, 3);
+ expect(result[0].severity).toBe('critical');
+ });
+});
+
+// ─── detectSessionDrift ─────────────────────────────────────────────────────
+
+describe('detectSessionDrift', () => {
+ it('detects session lasting longer than threshold', () => {
+ const start = new Date('2026-01-01T10:00:00Z').getTime();
+ const entries = [
+ makeEntry({ sessionId: 'sess-long', timestamp: new Date(start).toISOString() }),
+ makeEntry({ sessionId: 'sess-long', timestamp: new Date(start + 25 * 3600_000).toISOString() }),
+ ];
+ const result = detectSessionDrift(entries);
+ expect(result.length).toBeGreaterThanOrEqual(1);
+ expect(result[0].type).toBe('session_drift');
+ expect(result[0].sessionId).toBe('sess-long');
+ });
+
+ it('detects large gaps within a session', () => {
+ const start = new Date('2026-01-01T10:00:00Z').getTime();
+ const entries = [
+ makeEntry({ sessionId: 'sess-gap', action: 'auth.login', timestamp: new Date(start).toISOString() }),
+ makeEntry({ sessionId: 'sess-gap', action: 'tx.submit', timestamp: new Date(start + 45 * 60_000).toISOString() }),
+ ];
+ const result = detectSessionDrift(entries);
+ expect(result.length).toBeGreaterThanOrEqual(1);
+ expect(result.some((r) => r.type === 'session_gap')).toBe(true);
+ });
+
+ it('returns empty for entries without sessionId', () => {
+ const entries = [
+ makeEntry({ sessionId: undefined }),
+ makeEntry({ sessionId: undefined }),
+ ];
+ expect(detectSessionDrift(entries)).toHaveLength(0);
+ });
+
+ it('returns empty for short sessions', () => {
+ const start = new Date('2026-01-01T10:00:00Z').getTime();
+ const entries = [
+ makeEntry({ sessionId: 'short', timestamp: new Date(start).toISOString() }),
+ makeEntry({ sessionId: 'short', timestamp: new Date(start + 60_000).toISOString() }),
+ ];
+ expect(detectSessionDrift(entries)).toHaveLength(0);
+ });
+});
+
+// ─── detectCategoryConcentration ────────────────────────────────────────────
+
+describe('detectCategoryConcentration', () => {
+ it('detects when one category dominates', () => {
+ const entries = [];
+ for (let i = 0; i < 18; i++) entries.push(makeEntry({ category: 'security' }));
+ for (let i = 0; i < 2; i++) entries.push(makeEntry({ category: 'auth' }));
+ const result = detectCategoryConcentration(entries);
+ expect(result.length).toBeGreaterThanOrEqual(1);
+ expect(result[0].type).toBe('category_concentration');
+ expect(result[0].category).toBe('security');
+ });
+
+ it('returns empty for balanced distribution', () => {
+ const entries = [];
+ const cats = ['auth', 'transaction', 'security', 'wallet', 'system'];
+ for (let i = 0; i < 20; i++) {
+ entries.push(makeEntry({ category: cats[i % cats.length] }));
+ }
+ expect(detectCategoryConcentration(entries)).toHaveLength(0);
+ });
+
+ it('returns empty when fewer than 10 entries', () => {
+ const entries = makeEntries(5, { category: 'security' });
+ expect(detectCategoryConcentration(entries)).toHaveLength(0);
+ });
+});
+
+// ─── scoreEntry ──────────────────────────────────────────────────────────────
+
+describe('scoreEntry', () => {
+ it('returns 0 for info/success/system entry', () => {
+ const entry = makeEntry({ severity: 'info', outcome: 'success', category: 'system' });
+ expect(scoreEntry(entry)).toBe(0);
+ });
+
+ it('returns higher score for critical severity', () => {
+ const crit = makeEntry({ severity: 'critical', outcome: 'success', category: 'system' });
+ const info = makeEntry({ severity: 'info', outcome: 'success', category: 'system' });
+ expect(scoreEntry(crit)).toBeGreaterThan(scoreEntry(info));
+ });
+
+ it('applies category multiplier', () => {
+ const security = makeEntry({ severity: 'medium', outcome: 'success', category: 'security' });
+ const system = makeEntry({ severity: 'medium', outcome: 'success', category: 'system' });
+ expect(scoreEntry(security)).toBeGreaterThan(scoreEntry(system));
+ });
+
+ it('adds outcome penalty for failures', () => {
+ const success = makeEntry({ severity: 'high', outcome: 'success', category: 'auth' });
+ const failure = makeEntry({ severity: 'high', outcome: 'failure', category: 'auth' });
+ expect(scoreEntry(failure)).toBeGreaterThan(scoreEntry(success));
+ });
+
+ it('caps at maxRiskScore', () => {
+ const entry = makeEntry({ severity: 'critical', outcome: 'denied', category: 'security' });
+ expect(scoreEntry(entry)).toBeLessThanOrEqual(100);
+ });
+});
+
+// ─── computeAggregateRisk ───────────────────────────────────────────────────
+
+describe('computeAggregateRisk', () => {
+ it('returns score 0 / grade A for empty entries', () => {
+ const result = computeAggregateRisk([]);
+ expect(result.score).toBe(0);
+ expect(result.grade).toBe('A');
+ });
+
+ it('returns higher score with more anomalies', () => {
+ const entries = makeEntries(10, { severity: 'medium', outcome: 'success', category: 'auth' });
+ const low = computeAggregateRisk(entries, []);
+ const high = computeAggregateRisk(entries, Array.from({ length: 5 }, () => ({ type: 'test', severity: 'high' })));
+ expect(high.score).toBeGreaterThanOrEqual(low.score);
+ });
+
+ it('returns higher score with more failures', () => {
+ const successEntries = makeEntries(10, { outcome: 'success' });
+ const failureEntries = makeEntries(10, { outcome: 'failure' });
+ expect(computeAggregateRisk(failureEntries).score).toBeGreaterThan(
+ computeAggregateRisk(successEntries).score,
+ );
+ });
+
+ it('returns valid grades', () => {
+ const entries = makeEntries(10, { severity: 'critical', outcome: 'denied', category: 'security' });
+ const result = computeAggregateRisk(entries);
+ expect(['A', 'B', 'C', 'D', 'F']).toContain(result.grade);
+ });
+});
+
+// ─── riskGrade ──────────────────────────────────────────────────────────────
+
+describe('riskGrade', () => {
+ it('returns A for scores 0-10', () => {
+ expect(riskGrade(0)).toBe('A');
+ expect(riskGrade(10)).toBe('A');
+ });
+
+ it('returns B for scores 11-25', () => {
+ expect(riskGrade(11)).toBe('B');
+ expect(riskGrade(25)).toBe('B');
+ });
+
+ it('returns C for scores 26-50', () => {
+ expect(riskGrade(26)).toBe('C');
+ expect(riskGrade(50)).toBe('C');
+ });
+
+ it('returns D for scores 51-75', () => {
+ expect(riskGrade(51)).toBe('D');
+ expect(riskGrade(75)).toBe('D');
+ });
+
+ it('returns F for scores 76-100', () => {
+ expect(riskGrade(76)).toBe('F');
+ expect(riskGrade(100)).toBe('F');
+ });
+});
+
+// ─── generateReport ─────────────────────────────────────────────────────────
+
+describe('generateReport', () => {
+ it('returns empty report for empty entries', () => {
+ const report = generateReport([]);
+ expect(report.entryCount).toBe(0);
+ expect(report.findings).toHaveLength(0);
+ expect(report.recommendations).toHaveLength(0);
+ expect(report.riskScore.grade).toBe('A');
+ });
+
+ it('includes all required fields', () => {
+ const report = generateReport(makeEntries(5));
+ expect(report).toHaveProperty('timestamp');
+ expect(report).toHaveProperty('entryCount');
+ expect(report).toHaveProperty('riskScore');
+ expect(report).toHaveProperty('findings');
+ expect(report).toHaveProperty('summary');
+ expect(report).toHaveProperty('recommendations');
+ expect(report).toHaveProperty('entryStats');
+ expect(report).toHaveProperty('timeRange');
+ });
+
+ it('sorts findings by severity (highest first)', () => {
+ const start = Date.now();
+ const entries = [];
+ // Create entries that will trigger multiple finding types
+ for (let i = 0; i < 5; i++) {
+ entries.push(makeEntry({
+ severity: 'info',
+ outcome: 'success',
+ timestamp: new Date(start + i * 1000).toISOString(),
+ }));
+ }
+ for (let i = 0; i < 5; i++) {
+ entries.push(makeEntry({
+ severity: 'critical',
+ outcome: 'failure',
+ timestamp: new Date(start + 5000 + i * 1000).toISOString(),
+ }));
+ }
+ const report = generateReport(entries);
+ for (let i = 1; i < report.findings.length; i++) {
+ const sevOrder = { critical: 4, high: 3, medium: 2, low: 1, info: 0 };
+ const prev = sevOrder[report.findings[i - 1].severity] ?? 0;
+ const curr = sevOrder[report.findings[i].severity] ?? 0;
+ expect(prev).toBeGreaterThanOrEqual(curr);
+ }
+ });
+
+ it('aggregates findings by type and severity', () => {
+ const start = Date.now();
+ const entries = [];
+ // Create many off-hours entries
+ for (let i = 0; i < 5; i++) {
+ entries.push(makeEntry({
+ timestamp: new Date(new Date('2026-01-01T23:00:00Z').getTime() + i * 60_000).toISOString(),
+ }));
+ }
+ const report = generateReport(entries);
+ expect(report.summary).toHaveProperty('totalFindings');
+ expect(report.summary).toHaveProperty('byType');
+ expect(report.summary).toHaveProperty('bySeverity');
+ });
+
+ it('includes entry-level stats', () => {
+ const entries = [
+ makeEntry({ severity: 'high', category: 'auth', outcome: 'failure' }),
+ makeEntry({ severity: 'info', category: 'system', outcome: 'success' }),
+ ];
+ const report = generateReport(entries);
+ expect(report.entryStats.bySeverity.high).toBe(1);
+ expect(report.entryStats.byCategory.auth).toBe(1);
+ expect(report.entryStats.byOutcome.failure).toBe(1);
+ });
+
+ it('provides recommendations based on findings', () => {
+ const start = new Date('2026-01-01T10:00:00Z').getTime();
+ const entries = [];
+ // Create repeated failures
+ for (let i = 0; i < 5; i++) {
+ entries.push(makeEntry({
+ outcome: 'failure',
+ action: 'auth.login',
+ actor: 'attacker',
+ timestamp: new Date(start + i * 5000).toISOString(),
+ }));
+ }
+ const report = generateReport(entries);
+ expect(report.recommendations.length).toBeGreaterThan(0);
+ });
+});
+
+// ─── createStreamingEngine ──────────────────────────────────────────────────
+
+describe('createStreamingEngine', () => {
+ let engine;
+
+ beforeEach(() => {
+ engine = createStreamingEngine();
+ });
+
+ it('starts with empty buffer', () => {
+ expect(engine.buffer).toHaveLength(0);
+ });
+
+ it('adds entries to buffer', () => {
+ engine.addEntry(makeEntry());
+ engine.addEntry(makeEntry());
+ expect(engine.buffer).toHaveLength(2);
+ });
+
+ it('returns cached findings until buffer changes', () => {
+ engine.addEntry(makeEntry());
+ const f1 = engine.getFindings();
+ const f2 = engine.getFindings();
+ expect(f1).toBe(f2); // Same reference (cached)
+ });
+
+ it('invalidates cache when new entry is added', () => {
+ engine.addEntry(makeEntry());
+ const f1 = engine.getFindings();
+ engine.addEntry(makeEntry());
+ const f2 = engine.getFindings();
+ expect(f1).not.toBe(f2); // Different reference (cache invalidated)
+ });
+
+ it('returns valid risk score', () => {
+ engine.addEntry(makeEntry({ severity: 'info', outcome: 'success' }));
+ const risk = engine.getRiskScore();
+ expect(risk).toHaveProperty('score');
+ expect(risk).toHaveProperty('grade');
+ expect(risk.score).toBeGreaterThanOrEqual(0);
+ expect(risk.score).toBeLessThanOrEqual(100);
+ });
+
+ it('returns valid report', () => {
+ engine.addEntry(makeEntry());
+ const report = engine.getReport();
+ expect(report).toHaveProperty('entryCount', 1);
+ expect(report).toHaveProperty('findings');
+ expect(report).toHaveProperty('riskScore');
+ });
+
+ it('reset clears the buffer', () => {
+ engine.addEntry(makeEntry());
+ engine.addEntry(makeEntry());
+ engine.reset();
+ expect(engine.buffer).toHaveLength(0);
+ });
+
+ it('caps buffer at 2000 entries', () => {
+ for (let i = 0; i < 2100; i++) {
+ engine.addEntry(makeEntry());
+ }
+ expect(engine.buffer.length).toBeLessThanOrEqual(2000);
+ });
+
+ it('analyzes entries in real-time as they arrive', () => {
+ const start = Date.now();
+ // Add entries that create a repeated failure pattern
+ for (let i = 0; i < 5; i++) {
+ engine.addEntry(makeEntry({
+ outcome: 'failure',
+ action: 'tx.submit',
+ actor: 'user-1',
+ timestamp: new Date(start + i * 3000).toISOString(),
+ }));
+ }
+ const report = engine.getReport();
+ expect(report.summary.totalFindings).toBeGreaterThanOrEqual(1);
+ });
+});
+
+// ─── Integration: full pipeline ─────────────────────────────────────────────
+
+describe('full analysis pipeline', () => {
+ it('generates a comprehensive report from realistic audit data', () => {
+ const start = new Date('2026-01-01T10:00:00Z').getTime();
+ const entries = [];
+
+ // Normal activity
+ for (let i = 0; i < 20; i++) {
+ entries.push(makeEntry({
+ timestamp: new Date(start + i * 30_000).toISOString(),
+ severity: 'info',
+ outcome: 'success',
+ category: i % 3 === 0 ? 'auth' : 'transaction',
+ actor: `user-${i % 5}`,
+ }));
+ }
+
+ // Off-hours activity
+ entries.push(makeEntry({
+ timestamp: '2026-01-01T23:30:00Z',
+ severity: 'high',
+ outcome: 'failure',
+ category: 'security',
+ actor: 'unknown',
+ }));
+
+ // Repeated failures
+ for (let i = 0; i < 5; i++) {
+ entries.push(makeEntry({
+ timestamp: new Date(start + 1_000_000 + i * 5000).toISOString(),
+ severity: 'medium',
+ outcome: 'failure',
+ action: 'auth.login',
+ actor: 'brute-forcer',
+ }));
+ }
+
+ const report = generateReport(entries);
+ expect(report.entryCount).toBe(entries.length);
+ expect(report.findings.length).toBeGreaterThan(0);
+ expect(report.riskScore.score).toBeGreaterThanOrEqual(0);
+ expect(report.recommendations.length).toBeGreaterThan(0);
+ expect(report.timeRange).toBeDefined();
+ });
+});