Compare commits

...

4 commits

Author SHA1 Message Date
Viktor Torstensson
fba30046eb
Merge pull request #1372 from lightninglabs/frontend-dep-audit
Some checks are pending
CI / frontend tests on macOS-latest (push) Waiting to run
CI / frontend tests on ubuntu-latest (push) Waiting to run
CI / frontend tests on windows-latest (push) Waiting to run
CI / backend build on macOS-latest (push) Waiting to run
CI / backend build on ubuntu-latest (push) Waiting to run
CI / backend build on windows-latest (push) Waiting to run
CI / cross compilation (push) Waiting to run
CI / cross compilation-1 (push) Waiting to run
CI / cross compilation-2 (push) Waiting to run
CI / RPC proto compilation check (push) Waiting to run
CI / check commits (push) Waiting to run
CI / Sqlc check (push) Waiting to run
CI / lint (push) Waiting to run
CI / run unit tests (push) Waiting to run
CI / run unit tests-1 (push) Waiting to run
CI / run unit tests-2 (push) Waiting to run
CI / run unit tests-3 (push) Waiting to run
CI / build itest binaries (push) Waiting to run
CI / integration test (push) Blocked by required conditions
CI / integration test-1 (push) Blocked by required conditions
CI / integration test-2 (push) Blocked by required conditions
CI / check release notes updated (push) Waiting to run
app: update outdated frontend dependencies
2026-08-13 11:51:33 +02:00
jamaljsr
a04ecb750d
ci: run node 24
The frontend job was pinned to node 16, which is old enough that some of
the packages patched in this branch refuse to install there. tar and
esbuild want node 18 or newer, serialize-javascript wants 20.

Node was also only pinned in the frontend job. The backend,
proto-compile-check and itest jobs all reference matrix.node_version
without declaring it in their matrix, so the expression resolved to an
empty string and setup-node fell back to whatever the runner happened to
ship. Two of those run `yarn install --frozen-lockfile`, so they need a
known version too.

Both are fixed by a NODE_VERSION env var, matching how GO_VERSION is
already handled. It replaces the matrix entry rather than feeding it,
because the env context isn't available inside strategy.matrix.
2026-08-11 10:12:44 -05:00
jamaljsr
5ed07dbf71
app: patch vulnerable dev dependencies
Sweeps the build-time dependency tree, which is where most of what
`yarn audit` reports actually lives. This takes the frontend from 419
findings across 42 packages down to 16 across 9.

Most of it is lockfile-only. `yarn upgrade` does not re-resolve
transitive entries in yarn 1, so the affected entries were dropped from
yarn.lock and reinstalled, which picks the newest version each parent's
existing range already allows. That covers the four open dependabot PRs
(shell-quote, websocket-driver, @babel/core and
@babel/plugin-transform-modules-systemjs) along with @babel/helpers,
ajv, async, brace-expansion, form-data, glob, js-yaml, json5, nanoid,
terser and ws.

Re-resolving express to 4.22.2 pulls patched body-parser, cookie,
path-to-regexp and qs with it, and clears the stale minimatch 3.0.4 and
semver 7.0.0 pins at the same time.

browserslist and caniuse-lite had to move too. @babel/core 7.29 emits
'opera_mobile' as a compilation target and the pinned
@babel/helper-compilation-targets 7.17.7 rejects it, which broke the
production build until the whole toolchain was aligned.

storybook goes to 7.6.24 and webpack to 5.109.2, both direct
devDependencies pinned to vulnerable versions.

The `resolutions` additions are for packages whose parent pins a range
with no patched version in it. Each was verified against a build:

- svgo 2.8.3 removes the svgo 1.x subtree that @svgr/plugin-svgo drags
  in. react-scripts sets `svgo: false`, so that plugin never runs, and
  the production bundle is byte for byte identical with and without it.
- resolve-url-loader 5.0.0 is the only way to drop postcss 7. The
  emitted CSS is unchanged, same content hash.
- @tootallnate/once, esbuild, nth-check, serialize-javascript, tar and
  uuid are straightforward version forces.

What is left is either unfixable or would break the build:

- protoc-gen-js pulls got, http-cache-semantics, adm-zip and decompress
  through the unmaintained `download` package. decompress has no
  published fix. None of it runs outside `yarn protos`.
- webpack-dev-server 5 drops the onBeforeSetupMiddleware and
  onAfterSetupMiddleware hooks that react-scripts 5.0.1 calls directly,
  so forcing it would break `yarn start`.
- ip and lodash.pick have no patched version published.
- react-router needs v7, which is a separate migration.
2026-08-11 10:12:44 -05:00
jamaljsr
b6646d7868
app: patch vulnerable production dependencies
Bumps the two production dependencies flagged by `yarn audit` that have
fixes available.

http-proxy-middleware goes to 2.0.10 for the Host-header routing bypass.
It also needs a `resolutions` entry, otherwise yarn keeps a second copy
at 2.0.9 under react-scripts > webpack-dev-server because the existing
`^2.0.0` range is already satisfied.

react-router-dom goes to 6.30.4 for the untrusted-path open redirect. It
is pinned exactly rather than with a caret: @remix-run/router is a direct
dependency at an exact version and react-router-dom pins its own copy
exactly too, so a caret here would let a future patch release install a
second copy of the router and hand <HistoryRouter> a history built by a
different module instance.

That bump pulls in a few related changes:

- react-router 6.4 folded the `history` package into @remix-run/router,
  so the store now imports `createBrowserHistory` from there. Passing our
  own history object is discouraged and would bundle history twice.
- @remix-run/router's type declarations use accessors in interfaces,
  which needs TypeScript 4.3 or newer, so typescript goes to 4.9.5. TS
  4.4 also began typing `catch` variables as `unknown`, so `handleError`
  now takes `unknown` and narrows before reading `.message`, and the five
  sites that read it directly narrow first as well.
- @types/history and @types/react-router-dom are v5-era leftovers that
  now conflict with the types react-router-dom ships itself.
- The new history only accepts one active listener, but RouterStore and
  <HistoryRouter> both need one, so RouterStore subscribes once and fans
  updates out. It also needs `v5Compat` so listeners fire on push() and
  replace() rather than only on back/forward.

reactour still pulls in a vulnerable lodash.pick with no patched version
published. reactour 1.18.0 is the end of the v1 line and v2 is a rewrite,
so that one is left alone for now.
2026-08-11 10:12:44 -05:00
11 changed files with 2206 additions and 2492 deletions

View file

@ -26,6 +26,11 @@ env:
# /dev.Dockerfile
GO_VERSION: 1.25.11
# Only applies to the CI jobs below. The note above covers GO_VERSION alone:
# /Dockerfile and /dev.Dockerfile pin their own Node version for release
# builds, and it does not have to match this one.
NODE_VERSION: 24.x
jobs:
########################
# frontend build checks
@ -36,7 +41,6 @@ jobs:
strategy:
matrix:
node_version: [ 16.x ]
os: [ ubuntu-latest, windows-latest, macOS-latest ]
steps:
@ -45,10 +49,10 @@ jobs:
with:
fetch-depth: 0
- name: setup nodejs ${{ matrix.node_version }}
- name: setup nodejs ${{ env.NODE_VERSION }}
uses: ./.github/actions/setup-node
with:
node-version: '${{ matrix.node_version }}'
node-version: '${{ env.NODE_VERSION }}'
- name: install dependencies
working-directory: ./app
@ -84,10 +88,10 @@ jobs:
with:
fetch-depth: 0
- name: setup nodejs ${{ matrix.node_version }}
- name: setup nodejs ${{ env.NODE_VERSION }}
uses: ./.github/actions/setup-node
with:
node-version: '${{ matrix.node_version }}'
node-version: '${{ env.NODE_VERSION }}'
- name: setup go ${{ env.GO_VERSION }}
uses: ./.github/actions/setup-go
@ -144,10 +148,10 @@ jobs:
with:
fetch-depth: 0
- name: setup nodejs ${{ matrix.node_version }}
- name: setup nodejs ${{ env.NODE_VERSION }}
uses: ./.github/actions/setup-node
with:
node-version: '${{ matrix.node_version }}'
node-version: '${{ env.NODE_VERSION }}'
- name: setup go ${{ env.GO_VERSION }}
uses: ./.github/actions/setup-go
@ -324,10 +328,10 @@ jobs:
with:
fetch-depth: 0
- name: setup nodejs ${{ matrix.node_version }}
- name: setup nodejs ${{ env.NODE_VERSION }}
uses: ./.github/actions/setup-node
with:
node-version: '${{ matrix.node_version }}'
node-version: '${{ env.NODE_VERSION }}'
- name: setup go ${{ env.GO_VERSION }}
uses: ./.github/actions/setup-go

View file

@ -22,6 +22,7 @@
"@emotion/react": "11.4.0",
"@emotion/styled": "11.3.0",
"@improbable-eng/grpc-web": "0.14.0",
"@remix-run/router": "1.23.3",
"@types/react-collapse": "^5.0.1",
"big.js": "6.1.1",
"bootstrap": "4.6.1",
@ -31,7 +32,7 @@
"date-fns": "2.14.0",
"debug": "4.3.1",
"file-saver": "2.0.2",
"http-proxy-middleware": "2.0.9",
"http-proxy-middleware": "2.0.10",
"i18next": "19.5.1",
"i18next-browser-languagedetector": "5.0.0",
"lodash": "4.18.1",
@ -48,7 +49,7 @@
"react-collapse": "^5.1.1",
"react-dom": "17.0.2",
"react-i18next": "13.5.0",
"react-router-dom": "^6.3.0",
"react-router-dom": "6.30.4",
"react-toastify": "6.0.6",
"react-virtualized": "9.21.2",
"reactour": "1.18.0",
@ -56,13 +57,13 @@
"styled-components": "5.1.1"
},
"devDependencies": {
"@storybook/addon-actions": "7.5.3",
"@storybook/addon-docs": "7.5.3",
"@storybook/addon-links": "7.5.3",
"@storybook/addons": "7.5.3",
"@storybook/preset-create-react-app": "7.5.3",
"@storybook/react": "7.5.3",
"@storybook/react-webpack5": "7.5.3",
"@storybook/addon-actions": "7.6.24",
"@storybook/addon-docs": "7.6.24",
"@storybook/addon-links": "7.6.24",
"@storybook/addons": "7.6.24",
"@storybook/preset-create-react-app": "7.6.24",
"@storybook/react": "7.6.24",
"@storybook/react-webpack5": "7.6.24",
"@testing-library/jest-dom": "5.11.5",
"@testing-library/react": "11.1.1",
"@testing-library/user-event": "12.2.0",
@ -71,13 +72,11 @@
"@types/debug": "4.1.5",
"@types/file-saver": "2.0.1",
"@types/google-protobuf": "3.15.10",
"@types/history": "4.7.6",
"@types/jest": "27.4.1",
"@types/lodash": "4.14.157",
"@types/node": "14.0.14",
"@types/react": "17.0.13",
"@types/react-dom": "17.0.8",
"@types/react-router-dom": "^5.3.3",
"@types/react-virtualized": "9.21.10",
"@types/reactour": "1.17.1",
"@types/semver": "^7.3.9",
@ -94,14 +93,23 @@
"protoc-gen-js": "3.21.2",
"react-scripts": "^5.0.1",
"sass": "1.43.4",
"storybook": "7.5.3",
"storybook": "7.6.24",
"ts-protoc-gen": "0.15.0",
"typescript": "4.1.6",
"webpack": "5.89.0"
"typescript": "4.9.5",
"webpack": "5.109.2"
},
"resolutions": {
"strip-ansi": "6.0.1",
"@tootallnate/once": "2.0.1",
"esbuild": "0.25.12",
"http-proxy-middleware": "2.0.10",
"jackspeak": "2.1.1",
"nth-check": "2.1.1",
"resolve-url-loader": "5.0.0",
"serialize-javascript": "7.1.0",
"strip-ansi": "6.0.1",
"svgo": "2.8.3",
"tar": "7.5.22",
"uuid": "11.1.1",
"wrap-ansi": "7.0.0"
},
"eslintConfig": {

View file

@ -36,6 +36,16 @@ describe('AppView', () => {
expect(store.alerts.size).toBe(1);
});
it('should handle values thrown that are not Errors', () => {
// Nothing in the language guarantees a thrown value is an Error, and
// showing the user an empty alert is worse than showing the raw value.
store.handleError('something went wrong', 'title');
expect(store.alerts.size).toBe(1);
const alert = values(store.alerts)[0];
expect(alert.message).toBe('something went wrong');
expect(alert.title).toBe('title');
});
it('should handle authentication errors', () => {
rootStore.authStore.authenticated = true;
expect(store.alerts.size).toBe(0);

View file

@ -111,7 +111,7 @@ const AuthPage: React.FC = () => {
try {
await store.authStore.login(pass);
} catch (err) {
setError(err.message);
setError(err instanceof Error ? err.message : String(err));
const errors = store.authStore.errors;
setErrorDetailLit(errors.litDetail);
setErrorDetailLnd(errors.lndDetail);

View file

@ -1,6 +1,6 @@
import { autorun, makeAutoObservable, runInAction } from 'mobx';
import { IS_DEV, IS_TEST, USE_SAMPLE_DATA } from 'config';
import { createBrowserHistory, History } from 'history';
import { createBrowserHistory, History } from '@remix-run/router';
import AppStorage from 'util/appStorage';
import CsvExporter from 'util/csv';
import { actionLog, Logger } from 'util/log';
@ -217,7 +217,9 @@ export const createStore = (grpcClient?: GrpcClient, appStorage?: AppStorage) =>
const poolApi = new PoolApi(grpc);
const litApi = new LitApi(grpc);
const csv = new CsvExporter();
const history = createBrowserHistory();
// v5Compat is required for listeners to be notified of push() and replace()
// calls, not just browser back/forward navigation.
const history = createBrowserHistory({ v5Compat: true });
const store = new Store(
lndApi,

View file

@ -126,7 +126,9 @@ export default class AuthStore {
this.setCredentials('');
this._store.log.error('connection failure');
this.errors = { mainErr: '', litDetail: '', lndDetail: '' };
throw new Error(await this.getErrMsg(error.message));
throw new Error(
await this.getErrMsg(error instanceof Error ? error.message : String(error)),
);
}
}

View file

@ -150,7 +150,7 @@ export default class BatchStore {
this.loading = false;
});
} catch (error) {
if (error.message !== 'batch snapshot not found') {
if (!(error instanceof Error) || error.message !== 'batch snapshot not found') {
this._store.appView.handleError(error, `Unable to fetch batch with id ${prevId}`);
}
}
@ -171,7 +171,7 @@ export default class BatchStore {
this._store.log.info('updated batchStore.markets', toJS(this.markets));
});
} catch (error) {
if (error.message === 'batch snapshot not found') return;
if (error instanceof Error && error.message === 'batch snapshot not found') return;
this._store.appView.handleError(error, 'Unable to fetch the latest batch');
}
}

View file

@ -1,5 +1,8 @@
import { makeAutoObservable, runInAction } from 'mobx';
import { History, Location } from 'history';
import { History, Location } from '@remix-run/router';
/** the listener callback accepted by the history object */
type Listener = Parameters<History['listen']>[0];
export default class RouterStore {
/** the history object */
@ -11,13 +14,31 @@ export default class RouterStore {
constructor(history: History) {
makeAutoObservable(this, { history: false }, { deep: false, autoBind: true });
this.history = history;
this.location = history.location;
history.listen(({ location }) => {
// The router's history implementation only accepts a single active
// listener, but both this store and the <HistoryRouter> component need to
// observe navigation. Subscribe once here and fan the updates out to any
// additional listeners.
const listeners = new Set<Listener>();
history.listen(update => {
runInAction(() => {
this.location = location;
this.location = update.location;
});
listeners.forEach(listener => listener(update));
});
// Expose a history that hands out fan-out subscriptions instead of the
// single underlying one. A proxy is used so that the `location` and
// `action` getters continue to read through to the real history object.
this.history = new Proxy(history, {
get: (target, prop, receiver) => {
if (prop !== 'listen') return Reflect.get(target, prop, receiver);
return (listener: Listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
};
},
});
}

View file

@ -210,13 +210,13 @@ export default class AppView {
}
/** handle errors by showing a notification and/or the auth screen */
handleError(error: Error, title?: string) {
handleError(error: unknown, title?: string) {
if (error instanceof AuthenticationError) {
// this will automatically redirect to the auth page
this._store.authStore.authenticated = false;
this.notify(l('authErrorMsg'), l('authErrorTitle'));
} else {
this.notify(error.message, title);
this.notify(error instanceof Error ? error.message : String(error), title);
}
}
}

View file

@ -96,7 +96,10 @@ export default class AppStorage {
this.set(cacheKey, { expires, data });
log.info(`updated cache with ${keys.length} new ${cacheKey}`);
} catch (error) {
log.error(`failed to fetch ${cacheKey} from the API`, error.message);
log.error(
`failed to fetch ${cacheKey} from the API`,
error instanceof Error ? error.message : String(error),
);
}
}

File diff suppressed because it is too large Load diff