Compare commits

...

42 Commits

Author SHA1 Message Date
github-actions[bot]
5449e30eed chore(main): release 4.11.0 (#1519)
🤖 I have created a release *beep* *boop*
---


## [4.11.0](https://github.com/unraid/api/compare/v4.10.0...v4.11.0)
(2025-07-28)


### Features

* tailwind v4 ([#1522](https://github.com/unraid/api/issues/1522))
([2c62e0a](2c62e0ad09))
* **web:** install and configure nuxt ui
([#1524](https://github.com/unraid/api/issues/1524))
([407585c](407585cd40))


### Bug Fixes

* add missing breakpoints
([#1535](https://github.com/unraid/api/issues/1535))
([f5352e3](f5352e3a26))
* border color incorrect in tailwind
([#1544](https://github.com/unraid/api/issues/1544))
([f14b74a](f14b74af91))
* **connect:** omit extraneous fields during connect config validation
([#1538](https://github.com/unraid/api/issues/1538))
([45bd736](45bd73698b))
* **deps:** pin dependencies
([#1528](https://github.com/unraid/api/issues/1528))
([a74d935](a74d935b56))
* **deps:** pin dependency @nuxt/ui to 3.2.0
([#1532](https://github.com/unraid/api/issues/1532))
([8279531](8279531f2b))
* **deps:** update all non-major dependencies
([#1510](https://github.com/unraid/api/issues/1510))
([1a8da6d](1a8da6d92b))
* **deps:** update all non-major dependencies
([#1520](https://github.com/unraid/api/issues/1520))
([e2fa648](e2fa648d1c))
* inject Tailwind CSS into client entry point
([#1537](https://github.com/unraid/api/issues/1537))
([86b6c4f](86b6c4f85b))
* make settings grid responsive
([#1463](https://github.com/unraid/api/issues/1463))
([9dfdb8d](9dfdb8dce7))
* **notifications:** gracefully handle & mask invalid notifications
([#1529](https://github.com/unraid/api/issues/1529))
([05056e7](05056e7ca1))
* truncate log files when they take up more than 5mb of space
([#1530](https://github.com/unraid/api/issues/1530))
([0a18b38](0a18b38008))
* use async for primary file read/writes
([#1531](https://github.com/unraid/api/issues/1531))
([23b2b88](23b2b88461))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-28 11:55:29 -04:00
Eli Bosley
dc12656f81 chore: remove codeowners in favor of coderabbit suggested reviewers (#1545)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
  * Removed the code ownership assignments from the repository.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-28 11:50:22 -04:00
Eli Bosley
f14b74af91 fix: border color incorrect in tailwind (#1544)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Introduced new UI theme variables for border color, border radius, and
primary color states to enhance customization options.

* **Refactor**
* Removed redundant and unused CSS variables related to primary color
and border radius for improved consistency and maintainability.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-28 11:33:22 -04:00
renovate[bot]
e2fa648d1c fix(deps): update all non-major dependencies (#1520)
This PR contains the following updates:

| Package | Change | Age | Confidence | Type | Update |
|---|---|---|---|---|---|
| [@eslint/js](https://eslint.org)
([source](https://redirect.github.com/eslint/eslint/tree/HEAD/packages/js))
| [`9.31.0` ->
`9.32.0`](https://renovatebot.com/diffs/npm/@eslint%2fjs/9.31.0/9.32.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@eslint%2fjs/9.32.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@eslint%2fjs/9.31.0/9.32.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
|
[@graphql-tools/merge](https://redirect.github.com/ardatan/graphql-tools)
([source](https://redirect.github.com/ardatan/graphql-tools/tree/HEAD/packages/merge))
| [`9.0.24` ->
`9.1.1`](https://renovatebot.com/diffs/npm/@graphql-tools%2fmerge/9.0.24/9.1.1)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@graphql-tools%2fmerge/9.1.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@graphql-tools%2fmerge/9.0.24/9.1.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | minor |
|
[@graphql-tools/schema](https://redirect.github.com/ardatan/graphql-tools)
([source](https://redirect.github.com/ardatan/graphql-tools/tree/HEAD/packages/schema))
| [`10.0.23` ->
`10.0.25`](https://renovatebot.com/diffs/npm/@graphql-tools%2fschema/10.0.23/10.0.25)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@graphql-tools%2fschema/10.0.25?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@graphql-tools%2fschema/10.0.23/10.0.25?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
|
[@graphql-tools/utils](https://redirect.github.com/ardatan/graphql-tools)
([source](https://redirect.github.com/ardatan/graphql-tools/tree/HEAD/packages/utils))
| [`10.8.6` ->
`10.9.1`](https://renovatebot.com/diffs/npm/@graphql-tools%2futils/10.8.6/10.9.1)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@graphql-tools%2futils/10.9.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@graphql-tools%2futils/10.8.6/10.9.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| peerDependencies | minor |
|
[@graphql-tools/utils](https://redirect.github.com/ardatan/graphql-tools)
([source](https://redirect.github.com/ardatan/graphql-tools/tree/HEAD/packages/utils))
| [`10.8.6` ->
`10.9.1`](https://renovatebot.com/diffs/npm/@graphql-tools%2futils/10.8.6/10.9.1)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@graphql-tools%2futils/10.9.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@graphql-tools%2futils/10.8.6/10.9.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
|
[@graphql-tools/utils](https://redirect.github.com/ardatan/graphql-tools)
([source](https://redirect.github.com/ardatan/graphql-tools/tree/HEAD/packages/utils))
| [`10.8.6` ->
`10.9.1`](https://renovatebot.com/diffs/npm/@graphql-tools%2futils/10.8.6/10.9.1)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@graphql-tools%2futils/10.9.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@graphql-tools%2futils/10.8.6/10.9.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| pnpm.overrides | minor |
|
[@graphql-tools/utils](https://redirect.github.com/ardatan/graphql-tools)
([source](https://redirect.github.com/ardatan/graphql-tools/tree/HEAD/packages/utils))
| [`10.8.6` ->
`10.9.1`](https://renovatebot.com/diffs/npm/@graphql-tools%2futils/10.8.6/10.9.1)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@graphql-tools%2futils/10.9.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@graphql-tools%2futils/10.8.6/10.9.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | minor |
|
[@internationalized/number](https://redirect.github.com/adobe/react-spectrum)
| [`3.6.3` ->
`3.6.4`](https://renovatebot.com/diffs/npm/@internationalized%2fnumber/3.6.3/3.6.4)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@internationalized%2fnumber/3.6.4?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@internationalized%2fnumber/3.6.3/3.6.4?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [@manypkg/cli](https://redirect.github.com/Thinkmill/manypkg)
([source](https://redirect.github.com/Thinkmill/manypkg/tree/HEAD/packages/cli))
| [`0.24.0` ->
`0.25.0`](https://renovatebot.com/diffs/npm/@manypkg%2fcli/0.24.0/0.25.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@manypkg%2fcli/0.25.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@manypkg%2fcli/0.24.0/0.25.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | minor |
| [@nestjs/common](https://nestjs.com)
([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/common))
| [`11.1.3` ->
`11.1.5`](https://renovatebot.com/diffs/npm/@nestjs%2fcommon/11.1.3/11.1.5)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fcommon/11.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fcommon/11.1.3/11.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| peerDependencies | patch |
| [@nestjs/common](https://nestjs.com)
([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/common))
| [`11.1.3` ->
`11.1.5`](https://renovatebot.com/diffs/npm/@nestjs%2fcommon/11.1.3/11.1.5)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fcommon/11.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fcommon/11.1.3/11.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
| [@nestjs/common](https://nestjs.com)
([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/common))
| [`11.1.3` ->
`11.1.5`](https://renovatebot.com/diffs/npm/@nestjs%2fcommon/11.1.3/11.1.5)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fcommon/11.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fcommon/11.1.3/11.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [@nestjs/core](https://nestjs.com)
([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/core))
| [`11.1.3` ->
`11.1.5`](https://renovatebot.com/diffs/npm/@nestjs%2fcore/11.1.3/11.1.5)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fcore/11.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fcore/11.1.3/11.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| peerDependencies | patch |
| [@nestjs/core](https://nestjs.com)
([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/core))
| [`11.1.3` ->
`11.1.5`](https://renovatebot.com/diffs/npm/@nestjs%2fcore/11.1.3/11.1.5)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fcore/11.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fcore/11.1.3/11.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
| [@nestjs/core](https://nestjs.com)
([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/core))
| [`11.1.3` ->
`11.1.5`](https://renovatebot.com/diffs/npm/@nestjs%2fcore/11.1.3/11.1.5)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fcore/11.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fcore/11.1.3/11.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [@nestjs/platform-fastify](https://nestjs.com)
([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/platform-fastify))
| [`11.1.3` ->
`11.1.5`](https://renovatebot.com/diffs/npm/@nestjs%2fplatform-fastify/11.1.3/11.1.5)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2fplatform-fastify/11.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2fplatform-fastify/11.1.3/11.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [@nestjs/testing](https://nestjs.com)
([source](https://redirect.github.com/nestjs/nest/tree/HEAD/packages/testing))
| [`11.1.3` ->
`11.1.5`](https://renovatebot.com/diffs/npm/@nestjs%2ftesting/11.1.3/11.1.5)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@nestjs%2ftesting/11.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nestjs%2ftesting/11.1.3/11.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
| [@nuxt/eslint](https://redirect.github.com/nuxt/eslint)
([source](https://redirect.github.com/nuxt/eslint/tree/HEAD/packages/module))
| [`1.5.2` ->
`1.7.1`](https://renovatebot.com/diffs/npm/@nuxt%2feslint/1.5.2/1.7.1) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/@nuxt%2feslint/1.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nuxt%2feslint/1.5.2/1.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
| [@nuxt/ui](https://ui.nuxt.com)
([source](https://redirect.github.com/nuxt/ui)) | [`3.2.0` ->
`3.3.0`](https://renovatebot.com/diffs/npm/@nuxt%2fui/3.2.0/3.3.0) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/@nuxt%2fui/3.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nuxt%2fui/3.2.0/3.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | minor |
| [@pinia/nuxt](https://pinia.vuejs.org/ssr/nuxt.html)
([source](https://redirect.github.com/vuejs/pinia)) | [`0.11.1` ->
`0.11.2`](https://renovatebot.com/diffs/npm/@pinia%2fnuxt/0.11.1/0.11.2)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@pinia%2fnuxt/0.11.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@pinia%2fnuxt/0.11.1/0.11.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [@rollup/rollup-linux-x64-gnu](https://rollupjs.org/)
([source](https://redirect.github.com/rollup/rollup)) | [`4.45.1` ->
`4.46.1`](https://renovatebot.com/diffs/npm/@rollup%2frollup-linux-x64-gnu/4.45.1/4.46.1)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@rollup%2frollup-linux-x64-gnu/4.46.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@rollup%2frollup-linux-x64-gnu/4.45.1/4.46.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| optionalDependencies | minor |
|
[@storybook/addon-docs](https://redirect.github.com/storybookjs/storybook/tree/next/code/addons/docs)
([source](https://redirect.github.com/storybookjs/storybook/tree/HEAD/code/addons/docs))
| [`9.0.17` ->
`9.0.18`](https://renovatebot.com/diffs/npm/@storybook%2faddon-docs/9.0.17/9.0.18)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@storybook%2faddon-docs/9.0.18?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@storybook%2faddon-docs/9.0.17/9.0.18?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
|
[@storybook/addon-links](https://redirect.github.com/storybookjs/storybook/tree/next/code/addons/links)
([source](https://redirect.github.com/storybookjs/storybook/tree/HEAD/code/addons/links))
| [`9.0.17` ->
`9.0.18`](https://renovatebot.com/diffs/npm/@storybook%2faddon-links/9.0.17/9.0.18)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@storybook%2faddon-links/9.0.18?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@storybook%2faddon-links/9.0.17/9.0.18?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
|
[@storybook/builder-vite](https://redirect.github.com/storybookjs/storybook/tree/next/code/builders/builder-vite/#readme)
([source](https://redirect.github.com/storybookjs/storybook/tree/HEAD/code/builders/builder-vite))
| [`9.0.17` ->
`9.0.18`](https://renovatebot.com/diffs/npm/@storybook%2fbuilder-vite/9.0.17/9.0.18)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@storybook%2fbuilder-vite/9.0.18?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@storybook%2fbuilder-vite/9.0.17/9.0.18?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
|
[@storybook/vue3-vite](https://redirect.github.com/storybookjs/storybook/tree/next/code/frameworks/vue3-vite)
([source](https://redirect.github.com/storybookjs/storybook/tree/HEAD/code/frameworks/vue3-vite))
| [`9.0.17` ->
`9.0.18`](https://renovatebot.com/diffs/npm/@storybook%2fvue3-vite/9.0.17/9.0.18)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@storybook%2fvue3-vite/9.0.18?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@storybook%2fvue3-vite/9.0.17/9.0.18?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
| [@swc/core](https://swc.rs)
([source](https://redirect.github.com/swc-project/swc)) | [`1.12.14` ->
`1.13.2`](https://renovatebot.com/diffs/npm/@swc%2fcore/1.12.14/1.13.2)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@swc%2fcore/1.13.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@swc%2fcore/1.12.14/1.13.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
|
[@types/bun](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/bun)
([source](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/bun))
| [`1.2.18` ->
`1.2.19`](https://renovatebot.com/diffs/npm/@types%2fbun/1.2.18/1.2.19)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@types%2fbun/1.2.19?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@types%2fbun/1.2.18/1.2.19?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
|
[@types/node](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node)
([source](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node))
| [`22.16.4` ->
`22.16.5`](https://renovatebot.com/diffs/npm/@types%2fnode/22.16.4/22.16.5)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@types%2fnode/22.16.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@types%2fnode/22.16.4/22.16.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
|
[@typescript-eslint/eslint-plugin](https://typescript-eslint.io/packages/eslint-plugin)
([source](https://redirect.github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin))
| [`8.37.0` ->
`8.38.0`](https://renovatebot.com/diffs/npm/@typescript-eslint%2feslint-plugin/8.37.0/8.38.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@typescript-eslint%2feslint-plugin/8.38.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@typescript-eslint%2feslint-plugin/8.37.0/8.38.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
|
[@vitejs/plugin-vue](https://redirect.github.com/vitejs/vite-plugin-vue/tree/main/packages/plugin-vue#readme)
([source](https://redirect.github.com/vitejs/vite-plugin-vue/tree/HEAD/packages/plugin-vue))
| [`6.0.0` ->
`6.0.1`](https://renovatebot.com/diffs/npm/@vitejs%2fplugin-vue/6.0.0/6.0.1)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@vitejs%2fplugin-vue/6.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vitejs%2fplugin-vue/6.0.0/6.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
|
[@vueuse/components](https://redirect.github.com/vueuse/vueuse/tree/main/packages/components#readme)
([source](https://redirect.github.com/vueuse/vueuse/tree/HEAD/packages/components))
| [`13.5.0` ->
`13.6.0`](https://renovatebot.com/diffs/npm/@vueuse%2fcomponents/13.5.0/13.6.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@vueuse%2fcomponents/13.6.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vueuse%2fcomponents/13.5.0/13.6.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | minor |
| [@vueuse/core](https://redirect.github.com/vueuse/vueuse)
([source](https://redirect.github.com/vueuse/vueuse/tree/HEAD/packages/core))
| [`13.5.0` ->
`13.6.0`](https://renovatebot.com/diffs/npm/@vueuse%2fcore/13.5.0/13.6.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@vueuse%2fcore/13.6.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vueuse%2fcore/13.5.0/13.6.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
| [@vueuse/core](https://redirect.github.com/vueuse/vueuse)
([source](https://redirect.github.com/vueuse/vueuse/tree/HEAD/packages/core))
| [`13.5.0` ->
`13.6.0`](https://renovatebot.com/diffs/npm/@vueuse%2fcore/13.5.0/13.6.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@vueuse%2fcore/13.6.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vueuse%2fcore/13.5.0/13.6.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | minor |
|
[@vueuse/integrations](https://redirect.github.com/vueuse/vueuse/tree/main/packages/integrations#readme)
([source](https://redirect.github.com/vueuse/vueuse/tree/HEAD/packages/integrations))
| [`13.5.0` ->
`13.6.0`](https://renovatebot.com/diffs/npm/@vueuse%2fintegrations/13.5.0/13.6.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@vueuse%2fintegrations/13.6.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vueuse%2fintegrations/13.5.0/13.6.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | minor |
|
[@vueuse/nuxt](https://redirect.github.com/vueuse/vueuse/tree/main/packages/nuxt#readme)
([source](https://redirect.github.com/vueuse/vueuse/tree/HEAD/packages/nuxt))
| [`13.5.0` ->
`13.6.0`](https://renovatebot.com/diffs/npm/@vueuse%2fnuxt/13.5.0/13.6.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@vueuse%2fnuxt/13.6.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vueuse%2fnuxt/13.5.0/13.6.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
| [dotenv](https://redirect.github.com/motdotla/dotenv) | [`17.2.0` ->
`17.2.1`](https://renovatebot.com/diffs/npm/dotenv/17.2.0/17.2.1) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/dotenv/17.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/dotenv/17.2.0/17.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [eslint](https://eslint.org)
([source](https://redirect.github.com/eslint/eslint)) | [`9.31.0` ->
`9.32.0`](https://renovatebot.com/diffs/npm/eslint/9.31.0/9.32.0) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/eslint/9.32.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/eslint/9.31.0/9.32.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
|
[eslint-config-prettier](https://redirect.github.com/prettier/eslint-config-prettier)
| [`10.1.5` ->
`10.1.8`](https://renovatebot.com/diffs/npm/eslint-config-prettier/10.1.5/10.1.8)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/eslint-config-prettier/10.1.8?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/eslint-config-prettier/10.1.5/10.1.8?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
|
[eslint-plugin-n](https://redirect.github.com/eslint-community/eslint-plugin-n)
| [`17.21.0` ->
`17.21.2`](https://renovatebot.com/diffs/npm/eslint-plugin-n/17.21.0/17.21.2)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/eslint-plugin-n/17.21.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/eslint-plugin-n/17.21.0/17.21.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
|
[eslint-plugin-prettier](https://redirect.github.com/prettier/eslint-plugin-prettier)
| [`5.5.1` ->
`5.5.3`](https://renovatebot.com/diffs/npm/eslint-plugin-prettier/5.5.1/5.5.3)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/eslint-plugin-prettier/5.5.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/eslint-plugin-prettier/5.5.1/5.5.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
|
[eslint-plugin-storybook](https://redirect.github.com/storybookjs/storybook/code/lib/eslint-plugin#readme)
([source](https://redirect.github.com/storybookjs/storybook/tree/HEAD/code/lib/eslint-plugin))
| [`9.0.17` ->
`9.0.18`](https://renovatebot.com/diffs/npm/eslint-plugin-storybook/9.0.17/9.0.18)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/eslint-plugin-storybook/9.0.18?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/eslint-plugin-storybook/9.0.17/9.0.18?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
|
[inquirer](https://redirect.github.com/SBoudrias/Inquirer.js/blob/main/packages/inquirer/README.md)
([source](https://redirect.github.com/SBoudrias/Inquirer.js)) |
[`12.7.0` ->
`12.8.2`](https://renovatebot.com/diffs/npm/inquirer/12.7.0/12.8.2) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/inquirer/12.8.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/inquirer/12.7.0/12.8.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | minor |
| [jiti](https://redirect.github.com/unjs/jiti) | [`2.4.2` ->
`2.5.1`](https://renovatebot.com/diffs/npm/jiti/2.4.2/2.5.1) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/jiti/2.5.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/jiti/2.4.2/2.5.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
| [jiti](https://redirect.github.com/unjs/jiti) | [`2.4.2` ->
`2.5.1`](https://renovatebot.com/diffs/npm/jiti/2.4.2/2.5.1) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/jiti/2.5.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/jiti/2.4.2/2.5.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| overrides | minor |
| [jose](https://redirect.github.com/panva/jose) | [`6.0.11` ->
`6.0.12`](https://renovatebot.com/diffs/npm/jose/6.0.11/6.0.12) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/jose/6.0.12?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/jose/6.0.11/6.0.12?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| peerDependencies | patch |
| [jose](https://redirect.github.com/panva/jose) | [`6.0.11` ->
`6.0.12`](https://renovatebot.com/diffs/npm/jose/6.0.11/6.0.12) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/jose/6.0.12?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/jose/6.0.11/6.0.12?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
| [jose](https://redirect.github.com/panva/jose) | [`6.0.11` ->
`6.0.12`](https://renovatebot.com/diffs/npm/jose/6.0.11/6.0.12) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/jose/6.0.12?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/jose/6.0.11/6.0.12?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [lucide-vue-next](https://lucide.dev)
([source](https://redirect.github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-vue-next))
| [`0.525.0` ->
`0.528.0`](https://renovatebot.com/diffs/npm/lucide-vue-next/0.525.0/0.528.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/lucide-vue-next/0.528.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/lucide-vue-next/0.525.0/0.528.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | minor |
| [marked](https://marked.js.org)
([source](https://redirect.github.com/markedjs/marked)) | [`16.0.0` ->
`16.1.1`](https://renovatebot.com/diffs/npm/marked/16.0.0/16.1.1) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/marked/16.1.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/marked/16.0.0/16.1.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | minor |
| [nest-commander](https://nest-commander.jaymcdoniel.dev)
([source](https://redirect.github.com/jmcdo29/nest-commander/tree/HEAD/pacakges/nest-commander))
| [`3.17.0` ->
`3.18.0`](https://renovatebot.com/diffs/npm/nest-commander/3.17.0/3.18.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/nest-commander/3.18.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/nest-commander/3.17.0/3.18.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | minor |
| [node](https://nodejs.org)
([source](https://redirect.github.com/nodejs/node)) | `22.17.0` ->
`22.17.1` |
[![age](https://developer.mend.io/api/mc/badges/age/node-version/node/v22.17.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/node-version/node/v22.17.0/v22.17.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| | patch |
| [node](https://redirect.github.com/actions/node-versions) | `22.17.0`
-> `22.17.1` |
[![age](https://developer.mend.io/api/mc/badges/age/github-releases/actions%2fnode-versions/22.17.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/github-releases/actions%2fnode-versions/22.17.0/22.17.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| uses-with | patch |
| [node](https://redirect.github.com/nodejs/node) |
`22.17.0-bookworm-slim` -> `22.17.1-bookworm-slim` |
[![age](https://developer.mend.io/api/mc/badges/age/docker/node/22.17.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/docker/node/22.17.0/22.17.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| final | patch |
| [reka-ui](https://redirect.github.com/unovue/reka-ui) | [`2.3.2` ->
`2.4.0`](https://renovatebot.com/diffs/npm/reka-ui/2.3.2/2.4.0) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/reka-ui/2.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/reka-ui/2.3.2/2.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | minor |
| [storybook](https://storybook.js.org)
([source](https://redirect.github.com/storybookjs/storybook/tree/HEAD/code/core))
| [`9.0.17` ->
`9.0.18`](https://renovatebot.com/diffs/npm/storybook/9.0.17/9.0.18) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/storybook/9.0.18?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/storybook/9.0.17/9.0.18?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
|
[tw-animate-css](https://redirect.github.com/Wombosvideo/tw-animate-css)
| [`1.3.5` ->
`1.3.6`](https://renovatebot.com/diffs/npm/tw-animate-css/1.3.5/1.3.6) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/tw-animate-css/1.3.6?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/tw-animate-css/1.3.5/1.3.6?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
|
[typescript-eslint](https://typescript-eslint.io/packages/typescript-eslint)
([source](https://redirect.github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint))
| [`8.37.0` ->
`8.38.0`](https://renovatebot.com/diffs/npm/typescript-eslint/8.37.0/8.38.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/typescript-eslint/8.38.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/typescript-eslint/8.37.0/8.38.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
|
[validate-npm-package-name](https://redirect.github.com/npm/validate-npm-package-name)
| [`6.0.1` ->
`6.0.2`](https://renovatebot.com/diffs/npm/validate-npm-package-name/6.0.1/6.0.2)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/validate-npm-package-name/6.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/validate-npm-package-name/6.0.1/6.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [vite](https://vite.dev)
([source](https://redirect.github.com/vitejs/vite/tree/HEAD/packages/vite))
| [`7.0.4` ->
`7.0.6`](https://renovatebot.com/diffs/npm/vite/7.0.4/7.0.6) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/vite/7.0.6?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vite/7.0.4/7.0.6?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
|
[vue](https://redirect.github.com/vuejs/core/tree/main/packages/vue#readme)
([source](https://redirect.github.com/vuejs/core)) | [`3.5.17` ->
`3.5.18`](https://renovatebot.com/diffs/npm/vue/3.5.17/3.5.18) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/vue/3.5.18?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vue/3.5.17/3.5.18?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
|
[vue](https://redirect.github.com/vuejs/core/tree/main/packages/vue#readme)
([source](https://redirect.github.com/vuejs/core)) | [`3.5.17` ->
`3.5.18`](https://renovatebot.com/diffs/npm/vue/3.5.17/3.5.18) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/vue/3.5.18?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vue/3.5.17/3.5.18?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| peerDependencies | patch |
|
[vue-i18n](https://redirect.github.com/intlify/vue-i18n/tree/master/packages/vue-i18n#readme)
([source](https://redirect.github.com/intlify/vue-i18n/tree/HEAD/packages/vue-i18n))
| [`11.1.9` ->
`11.1.11`](https://renovatebot.com/diffs/npm/vue-i18n/11.1.9/11.1.11) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/vue-i18n/11.1.11?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vue-i18n/11.1.9/11.1.11?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [vue-tsc](https://redirect.github.com/vuejs/language-tools)
([source](https://redirect.github.com/vuejs/language-tools/tree/HEAD/packages/tsc))
| [`3.0.1` ->
`3.0.4`](https://renovatebot.com/diffs/npm/vue-tsc/3.0.1/3.0.4) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/vue-tsc/3.0.4?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vue-tsc/3.0.1/3.0.4?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
| [vuetify](https://vuetifyjs.com)
([source](https://redirect.github.com/vuetifyjs/vuetify/tree/HEAD/packages/vuetify))
| [`3.9.0` ->
`3.9.2`](https://renovatebot.com/diffs/npm/vuetify/3.9.0/3.9.2) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/vuetify/3.9.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vuetify/3.9.0/3.9.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [wrangler](https://redirect.github.com/cloudflare/workers-sdk)
([source](https://redirect.github.com/cloudflare/workers-sdk/tree/HEAD/packages/wrangler))
| [`4.24.3` ->
`4.26.0`](https://renovatebot.com/diffs/npm/wrangler/4.24.3/4.26.0) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/wrangler/4.26.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/wrangler/4.24.3/4.26.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |

---

### Release Notes

<details>
<summary>eslint/eslint (@&#8203;eslint/js)</summary>

###
[`v9.32.0`](https://redirect.github.com/eslint/eslint/compare/v9.31.0...50de1ced9df2b1ee48ee6843c8cfe0f5d8edbc27)

[Compare
Source](https://redirect.github.com/eslint/eslint/compare/v9.31.0...v9.32.0)

</details>

<details>
<summary>ardatan/graphql-tools (@&#8203;graphql-tools/merge)</summary>

###
[`v9.1.1`](https://redirect.github.com/ardatan/graphql-tools/blob/HEAD/packages/merge/CHANGELOG.md#911)

[Compare
Source](https://redirect.github.com/ardatan/graphql-tools/compare/@graphql-tools/merge@9.1.0...@graphql-tools/merge@9.1.1)

##### Patch Changes

-
[#&#8203;7298](https://redirect.github.com/ardatan/graphql-tools/pull/7298)

[`984d542`](984d542b95)
Thanks [@&#8203;jdolle](https://redirect.github.com/jdolle)! -
dependencies updates:
  - Removed dependency
[`@theguild/federation-composition@^0.19.0`
↗︎](https://www.npmjs.com/package/@&#8203;theguild/federation-composition/v/0.19.0)
    (from `dependencies`)

-
[#&#8203;7298](https://redirect.github.com/ardatan/graphql-tools/pull/7298)

[`984d542`](984d542b95)
Thanks [@&#8203;jdolle](https://redirect.github.com/jdolle)! - Fix
"Named export 'OperationTypeNode' not found"

- Updated dependencies

\[[`32d0457`](32d0457f3f)]:
-
[@&#8203;graphql-tools/utils](https://redirect.github.com/graphql-tools/utils)@&#8203;10.9.1

###
[`v9.1.0`](https://redirect.github.com/ardatan/graphql-tools/blob/HEAD/packages/merge/CHANGELOG.md#910)

[Compare
Source](https://redirect.github.com/ardatan/graphql-tools/compare/@graphql-tools/merge@9.0.24...@graphql-tools/merge@9.1.0)

##### Minor Changes

-
[#&#8203;7249](https://redirect.github.com/ardatan/graphql-tools/pull/7249)

[`e5f98c2`](e5f98c231b)
Thanks [@&#8203;jdolle](https://redirect.github.com/jdolle)! - Support
repeatable [@&#8203;link-ed](https://redirect.github.com/link-ed)
federation directives;
  fix merging non-identical, repeatable directives

##### Patch Changes

-
[#&#8203;7249](https://redirect.github.com/ardatan/graphql-tools/pull/7249)

[`e5f98c2`](e5f98c231b)
Thanks [@&#8203;jdolle](https://redirect.github.com/jdolle)! -
dependencies updates:
  - Added dependency
[`@theguild/federation-composition@^0.16.0`
↗︎](https://www.npmjs.com/package/@&#8203;theguild/federation-composition/v/0.16.0)
    (to `dependencies`)

-
[#&#8203;7276](https://redirect.github.com/ardatan/graphql-tools/pull/7276)

[`3c21496`](3c21496330)
Thanks [@&#8203;renovate](https://redirect.github.com/apps/renovate)! -
dependencies updates:
  - Updated dependency
[`@theguild/federation-composition@^0.19.0`
↗︎](https://www.npmjs.com/package/@&#8203;theguild/federation-composition/v/0.19.0)
    (from `^0.16.0`, in `dependencies`)

- Updated dependencies

\[[`22af985`](22af98581e),

[`53db005`](53db00540c)]:
-
[@&#8203;graphql-tools/utils](https://redirect.github.com/graphql-tools/utils)@&#8203;10.9.0

</details>

<details>
<summary>ardatan/graphql-tools (@&#8203;graphql-tools/schema)</summary>

###
[`v10.0.25`](https://redirect.github.com/ardatan/graphql-tools/blob/HEAD/packages/schema/CHANGELOG.md#10025)

[Compare
Source](https://redirect.github.com/ardatan/graphql-tools/compare/@graphql-tools/schema@10.0.24...@graphql-tools/schema@10.0.25)

##### Patch Changes

- Updated dependencies

\[[`984d542`](984d542b95),

[`984d542`](984d542b95),

[`32d0457`](32d0457f3f)]:
-
[@&#8203;graphql-tools/merge](https://redirect.github.com/graphql-tools/merge)@&#8203;9.1.1
-
[@&#8203;graphql-tools/utils](https://redirect.github.com/graphql-tools/utils)@&#8203;10.9.1

###
[`v10.0.24`](https://redirect.github.com/ardatan/graphql-tools/blob/HEAD/packages/schema/CHANGELOG.md#10024)

[Compare
Source](https://redirect.github.com/ardatan/graphql-tools/compare/@graphql-tools/schema@10.0.23...@graphql-tools/schema@10.0.24)

##### Patch Changes

- Updated dependencies

\[[`e5f98c2`](e5f98c231b),

[`3c21496`](3c21496330),

[`e5f98c2`](e5f98c231b),

[`22af985`](22af98581e),

[`53db005`](53db00540c)]:
-
[@&#8203;graphql-tools/merge](https://redirect.github.com/graphql-tools/merge)@&#8203;9.1.0
-
[@&#8203;graphql-tools/utils](https://redirect.github.com/graphql-tools/utils)@&#8203;10.9.0

</details>

<details>
<summary>ardatan/graphql-tools (@&#8203;graphql-tools/utils)</summary>

###
[`v10.9.1`](https://redirect.github.com/ardatan/graphql-tools/blob/HEAD/packages/utils/CHANGELOG.md#1091)

[Compare
Source](https://redirect.github.com/ardatan/graphql-tools/compare/@graphql-tools/utils@10.9.0...@graphql-tools/utils@10.9.1)

##### Patch Changes

-
[`32d0457`](32d0457f3f)
Thanks [@&#8203;ardatan](https://redirect.github.com/ardatan)! - Fix
oneOf handling

###
[`v10.9.0`](https://redirect.github.com/ardatan/graphql-tools/blob/HEAD/packages/utils/CHANGELOG.md#1090)

[Compare
Source](https://redirect.github.com/ardatan/graphql-tools/compare/@graphql-tools/utils@10.8.6...@graphql-tools/utils@10.9.0)

##### Minor Changes

-
[#&#8203;7281](https://redirect.github.com/ardatan/graphql-tools/pull/7281)

[`53db005`](53db00540c)
Thanks [@&#8203;EmrysMyrddin](https://redirect.github.com/EmrysMyrddin)!
- Add optional `subgraphName` preoperty
to the `ExecutionRequest` interface for usage in Gateways like Hive
Gateway.

##### Patch Changes

-
[#&#8203;7282](https://redirect.github.com/ardatan/graphql-tools/pull/7282)

[`22af985`](22af98581e)
Thanks [@&#8203;renovate](https://redirect.github.com/apps/renovate)! -
Support `@oneOf` directive

</details>

<details>
<summary>adobe/react-spectrum
(@&#8203;internationalized/number)</summary>

###
[`v3.6.4`](https://redirect.github.com/adobe/react-spectrum/compare/@internationalized/number@3.6.3...@internationalized/number@3.6.4)

[Compare
Source](https://redirect.github.com/adobe/react-spectrum/compare/@internationalized/number@3.6.3...@internationalized/number@3.6.4)

</details>

<details>
<summary>Thinkmill/manypkg (@&#8203;manypkg/cli)</summary>

###
[`v0.25.0`](https://redirect.github.com/Thinkmill/manypkg/blob/HEAD/packages/cli/CHANGELOG.md#0250)

[Compare
Source](https://redirect.github.com/Thinkmill/manypkg/compare/@manypkg/cli@0.24.0...@manypkg/cli@0.25.0)

##### Minor Changes

- [#&#8203;254](https://redirect.github.com/Thinkmill/manypkg/pull/254)
[`2c06ac0`](2c06ac0939)
Thanks [@&#8203;cjkihl](https://redirect.github.com/cjkihl)! - Add Bun
support

##### Patch Changes

- Updated dependencies
\[[`2c06ac0`](2c06ac0939)]:
-
[@&#8203;manypkg/get-packages](https://redirect.github.com/manypkg/get-packages)@&#8203;3.1.0

</details>

<details>
<summary>nestjs/nest (@&#8203;nestjs/common)</summary>

###
[`v11.1.5`](https://redirect.github.com/nestjs/nest/compare/v11.1.4...9bb0560e79743cc0bd2ce198c65e21332200c3ad)

[Compare
Source](https://redirect.github.com/nestjs/nest/compare/v11.1.4...v11.1.5)

###
[`v11.1.4`](https://redirect.github.com/nestjs/nest/compare/v11.1.3...1f101ac8b0a5bb5b97a7caf6634fcea8d65196e0)

[Compare
Source](https://redirect.github.com/nestjs/nest/compare/v11.1.3...v11.1.4)

</details>

<details>
<summary>nestjs/nest (@&#8203;nestjs/core)</summary>

###
[`v11.1.5`](https://redirect.github.com/nestjs/nest/compare/v11.1.4...9bb0560e79743cc0bd2ce198c65e21332200c3ad)

[Compare
Source](https://redirect.github.com/nestjs/nest/compare/v11.1.4...v11.1.5)

###
[`v11.1.4`](https://redirect.github.com/nestjs/nest/releases/tag/v11.1.4)

[Compare
Source](https://redirect.github.com/nestjs/nest/compare/v11.1.3...v11.1.4)

##### v11.1.4 (2025-07-16)

##### Bug fixes

- `platform-fastify`
- [#&#8203;15385](https://redirect.github.com/nestjs/nest/pull/15385)
fix(testing): auto-init fastify adapter for middleware registration
([@&#8203;mag123c](https://redirect.github.com/mag123c))
- `core`, `testing`
- [#&#8203;15405](https://redirect.github.com/nestjs/nest/pull/15405)
fix(core): fix race condition in class dependency resolution
([@&#8203;hajekjiri](https://redirect.github.com/hajekjiri))
- `core`
- [#&#8203;15333](https://redirect.github.com/nestjs/nest/pull/15333)
fix(core): Make flattenRoutePath return a valid module
([@&#8203;gentunian](https://redirect.github.com/gentunian))
- `microservices`
- [#&#8203;15305](https://redirect.github.com/nestjs/nest/pull/15305)
fix(microservices): Revisit RMQ pattern matching with wildcards
([@&#8203;getlarge](https://redirect.github.com/getlarge))
- [#&#8203;15250](https://redirect.github.com/nestjs/nest/pull/15250)
fix(constants): update RMQ\_DEFAULT\_QUEUE to an empty string
([@&#8203;EeeasyCode](https://redirect.github.com/EeeasyCode))

##### Enhancements

- `platform-fastify`
- [#&#8203;14789](https://redirect.github.com/nestjs/nest/pull/14789)
feat(fastify): add decorator for custom schema
([@&#8203;piotrfrankowski](https://redirect.github.com/piotrfrankowski))
- `common`, `core`, `microservices`, `platform-express`,
`platform-fastify`, `websockets`
- [#&#8203;15386](https://redirect.github.com/nestjs/nest/pull/15386)
feat: enhance introspection capabilities
([@&#8203;kamilmysliwiec](https://redirect.github.com/kamilmysliwiec))
- `core`
- [#&#8203;15374](https://redirect.github.com/nestjs/nest/pull/15374)
feat: supporting fine async storage control
([@&#8203;Farenheith](https://redirect.github.com/Farenheith))

##### Dependencies

- `platform-ws`
- [#&#8203;15350](https://redirect.github.com/nestjs/nest/pull/15350)
chore(deps): bump ws from 8.18.2 to 8.18.3
([@&#8203;dependabot\[bot\]](https://redirect.github.com/apps/dependabot))
- `platform-fastify`
- [#&#8203;15278](https://redirect.github.com/nestjs/nest/pull/15278)
chore(deps): bump fastify from 5.3.3 to 5.4.0
([@&#8203;dependabot\[bot\]](https://redirect.github.com/apps/dependabot))

##### Committers: 11

- Alexey Filippov
([@&#8203;SocketSomeone](https://redirect.github.com/SocketSomeone))
- EFIcats ([@&#8203;ext4cats](https://redirect.github.com/ext4cats))
- Edouard Maleix
([@&#8203;getlarge](https://redirect.github.com/getlarge))
- JaeHo Jang ([@&#8203;mag123c](https://redirect.github.com/mag123c))
- Jiri Hajek
([@&#8203;hajekjiri](https://redirect.github.com/hajekjiri))
- Kamil Mysliwiec
([@&#8203;kamilmysliwiec](https://redirect.github.com/kamilmysliwiec))
- Khan / 이창민
([@&#8203;EeeasyCode](https://redirect.github.com/EeeasyCode))
- Peter F.
([@&#8203;piotrfrankowski](https://redirect.github.com/piotrfrankowski))
- Sebastian ([@&#8203;gentunian](https://redirect.github.com/gentunian))
- Thiago Oliveira Santos
([@&#8203;Farenheith](https://redirect.github.com/Farenheith))
- jochong ([@&#8203;jochongs](https://redirect.github.com/jochongs))

</details>

<details>
<summary>nestjs/nest (@&#8203;nestjs/platform-fastify)</summary>

###
[`v11.1.5`](https://redirect.github.com/nestjs/nest/releases/tag/v11.1.5)

[Compare
Source](https://redirect.github.com/nestjs/nest/compare/v11.1.4...v11.1.5)

#### v11.1.5 (2025-07-18)

##### Dependencies

- `platform-express`
- [#&#8203;15425](https://redirect.github.com/nestjs/nest/pull/15425)
chore(deps): bump multer from 2.0.1 to 2.0.2 in
/packages/platform-express
([@&#8203;dependabot\[bot\]](https://redirect.github.com/apps/dependabot))

###
[`v11.1.4`](https://redirect.github.com/nestjs/nest/releases/tag/v11.1.4)

[Compare
Source](https://redirect.github.com/nestjs/nest/compare/v11.1.3...v11.1.4)

#### v11.1.4 (2025-07-16)

##### Bug fixes

- `platform-fastify`
- [#&#8203;15385](https://redirect.github.com/nestjs/nest/pull/15385)
fix(testing): auto-init fastify adapter for middleware registration
([@&#8203;mag123c](https://redirect.github.com/mag123c))
- `core`, `testing`
- [#&#8203;15405](https://redirect.github.com/nestjs/nest/pull/15405)
fix(core): fix race condition in class dependency resolution
([@&#8203;hajekjiri](https://redirect.github.com/hajekjiri))
- `core`
- [#&#8203;15333](https://redirect.github.com/nestjs/nest/pull/15333)
fix(core): Make flattenRoutePath return a valid module
([@&#8203;gentunian](https://redirect.github.com/gentunian))
- `microservices`
- [#&#8203;15305](https://redirect.github.com/nestjs/nest/pull/15305)
fix(microservices): Revisit RMQ pattern matching with wildcards
([@&#8203;getlarge](https://redirect.github.com/getlarge))
- [#&#8203;15250](https://redirect.github.com/nestjs/nest/pull/15250)
fix(constants): update RMQ\_DEFAULT\_QUEUE to an empty string
([@&#8203;EeeasyCode](https://redirect.github.com/EeeasyCode))

##### Enhancements

- `platform-fastify`
- [#&#8203;14789](https://redirect.github.com/nestjs/nest/pull/14789)
feat(fastify): add decorator for custom schema
([@&#8203;piotrfrankowski](https://redirect.github.com/piotrfrankowski))
- `common`, `core`, `microservices`, `platform-express`,
`platform-fastify`, `websockets`
- [#&#8203;15386](https://redirect.github.com/nestjs/nest/pull/15386)
feat: enhance introspection capabilities
([@&#8203;kamilmysliwiec](https://redirect.github.com/kamilmysliwiec))
- `core`
- [#&#8203;15374](https://redirect.github.com/nestjs/nest/pull/15374)
feat: supporting fine async storage control
([@&#8203;Farenheith](https://redirect.github.com/Farenheith))

##### Dependencies

- `platform-ws`
- [#&#8203;15350](https://redirect.github.com/nestjs/nest/pull/15350)
chore(deps): bump ws from 8.18.2 to 8.18.3
([@&#8203;dependabot\[bot\]](https://redirect.github.com/apps/dependabot))
- `platform-fastify`
- [#&#8203;15278](https://redirect.github.com/nestjs/nest/pull/15278)
chore(deps): bump fastify from 5.3.3 to 5.4.0
([@&#8203;dependabot\[bot\]](https://redirect.github.com/apps/dependabot))

##### Committers: 11

- Alexey Filippov
([@&#8203;SocketSomeone](https://redirect.github.com/SocketSomeone))
- EFIcats ([@&#8203;ext4cats](https://redirect.github.com/ext4cats))
- Edouard Maleix
([@&#8203;getlarge](https://redirect.github.com/getlarge))
- JaeHo Jang ([@&#8203;mag123c](https://redirect.github.com/mag123c))
- Jiri Hajek
([@&#8203;hajekjiri](https://redirect.github.com/hajekjiri))
- Kamil Mysliwiec
([@&#8203;kamilmysliwiec](https://redirect.github.com/kamilmysliwiec))
- Khan / 이창민
([@&#8203;EeeasyCode](https://redirect.github.com/EeeasyCode))
- Peter F.
([@&#8203;piotrfrankowski](https://redirect.github.com/piotrfrankowski))
- Sebastian ([@&#8203;gentunian](https://redirect.github.com/gentunian))
- Thiago Oliveira Santos
([@&#8203;Farenheith](https://redirect.github.com/Farenheith))
- jochong ([@&#8203;jochongs](https://redirect.github.com/jochongs))

</details>

<details>
<summary>nestjs/nest (@&#8203;nestjs/testing)</summary>

###
[`v11.1.5`](https://redirect.github.com/nestjs/nest/compare/v11.1.4...9bb0560e79743cc0bd2ce198c65e21332200c3ad)

[Compare
Source](https://redirect.github.com/nestjs/nest/compare/v11.1.4...v11.1.5)

###
[`v11.1.4`](https://redirect.github.com/nestjs/nest/compare/v11.1.3...1f101ac8b0a5bb5b97a7caf6634fcea8d65196e0)

[Compare
Source](https://redirect.github.com/nestjs/nest/compare/v11.1.3...v11.1.4)

</details>

<details>
<summary>nuxt/eslint (@&#8203;nuxt/eslint)</summary>

###
[`v1.7.1`](https://redirect.github.com/nuxt/eslint/releases/tag/v1.7.1)

[Compare
Source](https://redirect.github.com/nuxt/eslint/compare/v1.7.0...v1.7.1)

#####    🐞 Bug Fixes

- Include `eslint-typegen.d.ts` in `nuxt.node.d.ts`, close
[#&#8203;596](https://redirect.github.com/nuxt/eslint/issues/596)  -  by
[@&#8203;antfu](https://redirect.github.com/antfu) in
[https://github.com/nuxt/eslint/issues/596](https://redirect.github.com/nuxt/eslint/issues/596)
[<samp>(ab74e)</samp>](https://redirect.github.com/nuxt/eslint/commit/ab74efd)

#####     [View changes on
GitHub](https://redirect.github.com/nuxt/eslint/compare/v1.7.0...v1.7.1)

###
[`v1.7.0`](https://redirect.github.com/nuxt/eslint/releases/tag/v1.7.0)

[Compare
Source](https://redirect.github.com/nuxt/eslint/compare/v1.6.0...v1.7.0)

#####    🚀 Features

- Upgrade eslint-plugin-unicorn  -  by
[@&#8203;antfu](https://redirect.github.com/antfu)
[<samp>(b3b7d)</samp>](https://redirect.github.com/nuxt/eslint/commit/b3b7d93)

#####     [View changes on
GitHub](https://redirect.github.com/nuxt/eslint/compare/v1.6.0...v1.7.0)

###
[`v1.6.0`](https://redirect.github.com/nuxt/eslint/releases/tag/v1.6.0)

[Compare
Source](https://redirect.github.com/nuxt/eslint/compare/v1.5.2...v1.6.0)

#####    🐞 Bug Fixes

- Bring back `eslint-plugin-import-x` as default, close
[#&#8203;590](https://redirect.github.com/nuxt/eslint/issues/590)  -  by
[@&#8203;antfu](https://redirect.github.com/antfu) in
[https://github.com/nuxt/eslint/issues/590](https://redirect.github.com/nuxt/eslint/issues/590)
[<samp>(e43d6)</samp>](https://redirect.github.com/nuxt/eslint/commit/e43d6de)

#####     [View changes on
GitHub](https://redirect.github.com/nuxt/eslint/compare/v1.5.2...v1.6.0)

</details>

<details>
<summary>nuxt/ui (@&#8203;nuxt/ui)</summary>

###
[`v3.3.0`](https://redirect.github.com/nuxt/ui/blob/HEAD/CHANGELOG.md#330-2025-07-24)

[Compare
Source](https://redirect.github.com/nuxt/ui/compare/v3.2.0...v3.3.0)

##### Features

- **CommandPalette:** add `footer` slot
([#&#8203;4457](https://redirect.github.com/nuxt/ui/issues/4457))
([63730d6](63730d684b))
- **Drawer:** add `nested` prop
([e2695ee](e2695ee7e4)),
closes [#&#8203;4320](https://redirect.github.com/nuxt/ui/issues/4320)
- **FileUpload:** new component
([#&#8203;4564](https://redirect.github.com/nuxt/ui/issues/4564))
([35dbe6c](35dbe6c2ab))
- **Input/Textarea:** add `default-value` prop
([#&#8203;4404](https://redirect.github.com/nuxt/ui/issues/4404))
([fb9e7bb](fb9e7bb856))
- **InputMenu:** emit `remove-tag` event
([#&#8203;4511](https://redirect.github.com/nuxt/ui/issues/4511))
([6ca7c8b](6ca7c8b7bf))
- **InputTags:** add `max-length` prop
([b96a1cc](b96a1ccbab)),
closes [#&#8203;4405](https://redirect.github.com/nuxt/ui/issues/4405)
- **Kbd:** add `color` prop & `soft` variant
([#&#8203;4549](https://redirect.github.com/nuxt/ui/issues/4549))
([f336600](f33660035f))
- **module:** add `theme.defaultVariants` option
([#&#8203;4400](https://redirect.github.com/nuxt/ui/issues/4400))
([35f90b9](35f90b9920))
- **Popover:** add `reference` prop
([b00e07f](b00e07f13d))
- **Table:** add `footer` support to display column summary
([#&#8203;4194](https://redirect.github.com/nuxt/ui/issues/4194))
([c355cac](c355cacd43))
- **Table:** add `style` to table and column `meta`
([#&#8203;4513](https://redirect.github.com/nuxt/ui/issues/4513))
([1db21d1](1db21d1b00))
- **Table:** add row `hover` event
([f903ec3](f903ec396f)),
closes [#&#8203;2435](https://redirect.github.com/nuxt/ui/issues/2435)
- **Table:** add support for `colspan` and `rowspan`
([#&#8203;4460](https://redirect.github.com/nuxt/ui/issues/4460))
([7ef1933](7ef19333f0))
- **Table:** add support for context menu
([f62c5ec](f62c5ec20c)),
closes [#&#8203;4259](https://redirect.github.com/nuxt/ui/issues/4259)
- **Tabs:** add badge on items
([#&#8203;4553](https://redirect.github.com/nuxt/ui/issues/4553))
([62ab016](62ab01655c))
- **Toast:** progress bar with Progress component
([ec569e4](ec569e427b))
- **Tooltip:** add `reference` prop
([69a7b95](69a7b957d5)),
closes [#&#8203;4430](https://redirect.github.com/nuxt/ui/issues/4430)

##### Bug Fixes

- **Button/Link:** merge `active-class` / `inactive-class` with app
config ([#&#8203;4446](https://redirect.github.com/nuxt/ui/issues/4446))
([9debce7](9debce737c))
- **Button:** add `active` styles to behave like `hover` on mobile
([df8f202](df8f20232f)),
closes [#&#8203;991](https://redirect.github.com/nuxt/ui/issues/991)
- **Carousel/Tree:** add type to button elements for accessibility
([#&#8203;4493](https://redirect.github.com/nuxt/ui/issues/4493))
([fc24e03](fc24e03cc4))
- **Carousel:** add `aria-current` attribute to active dot
([#&#8203;4447](https://redirect.github.com/nuxt/ui/issues/4447))
([1ba8a55](1ba8a55bcb))
- **Carousel:** improve accessibility
([55e06e9](55e06e97e7)),
closes [#&#8203;4494](https://redirect.github.com/nuxt/ui/issues/4494)
- **Carousel:** resolve plugins with page transitions
([#&#8203;4380](https://redirect.github.com/nuxt/ui/issues/4380))
([3b67d54](3b67d54833))
- **ColorPicker:** update color conversion logic
([#&#8203;4550](https://redirect.github.com/nuxt/ui/issues/4550))
([6b6ec8c](https://redirec

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/unraid/api).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4yMy4yIiwidXBkYXRlZEluVmVyIjoiNDEuNDMuNSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Eli Bosley <ekbosley@gmail.com>
2025-07-28 11:07:52 -04:00
Eli Bosley
3b00fec5fd chore: Remove legacy store modules and add new API key and reporting services (#1536)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added developer CLI tools for toggling GraphQL sandbox and modal
testing utilities.
* Introduced a "Show Activation Modal" developer component for UI
testing.
  * Added system initial setup detection and related GraphQL queries.
* Enhanced login and welcome pages with dynamic server info and initial
setup state.
  * Improved SSO button with internationalization and error handling.
* Added internal CLI admin API key management service and internal
GraphQL client service.
* Introduced comprehensive API report generation service for system and
service status.
* Added CLI commands and GraphQL mutations/queries for plugin and SSO
user management.
* Added new modal target components and improved teleport target
detection.

* **Enhancements**
* Refined modal dialog targeting and teleportation for flexible UI
placement.
* Updated modal components and stores for improved activation/welcome
modal control.
  * Improved plugin and SSO user management via CLI through GraphQL API.
* Refactored partner logo components to use props instead of store
dependencies.
  * Enhanced styling and accessibility for buttons and modals.
* Streamlined Tailwind CSS integration with shared styles and updated
theme variables.
* Improved GraphQL module configuration to avoid directive conflicts in
tests.
  * Adjusted Vite config for better dependency handling in test mode.
  * Improved error handling and logging in CLI commands and services.
* Reordered imports and refined component class bindings for UI
consistency.

* **Bug Fixes**
* Resolved issues with duplicate script tags and component registration
in the web UI.
* Fixed modal close button visibility and activation modal state
handling.
* Added error handling and logging improvements across CLI commands and
services.
  * Fixed newline issues in last-download-time fixture files.

* **Chores**
* Added and updated numerous tests for CLI commands, services, and UI
components.
* Updated translation files and localization resources for new UI
messages.
* Adjusted environment, configuration, and dependency files for improved
development and test workflows.
  * Cleaned up unused imports and mocks in tests.
  * Reorganized exports and barrel files in shared and UI modules.
  * Added integration and dependency resolution tests for core modules.

* **Removals & Refactoring**
* Removed legacy Redux state management, configuration, and UPnP logic
from the backend.
* Eliminated deprecated GraphQL subscriptions and client code related to
registration and mothership.
* Removed direct store manipulation and replaced with service-based
approaches in CLI commands.
  * Deleted unused or redundant test files and configuration listeners.
* Refactored SSO user service to consolidate add/remove operations into
a single update method.
* Simplified API key services with new methods for automatic key
management.
* Replaced direct plugin and SSO user service calls with GraphQL client
interactions in CLI commands.
* Removed complex theme fallback and dark mode CSS rules, replacing with
streamlined static theme variables.
* Cleaned up Tailwind CSS configuration and removed deprecated local
styles.
* Removed multiple internal utility files and replaced with simplified
or centralized implementations.
* Removed deprecated local configuration and synchronization files and
listeners.
  * Removed UPnP helper functions and job management classes.
* Refactored server resolver to dynamically construct local server data
internally.
* Removed CORS handler and replaced with simplified or externalized
logic.
* Removed store synchronization and registration event pubsub handling.
* Removed GraphQL client creation utilities for internal API
communication.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-25 15:07:37 -04:00
Eli Bosley
4ff6a1aaa0 Add Claude Code GitHub Workflow (#1541)
## 🤖 Installing Claude Code GitHub App

This PR adds a GitHub Actions workflow that enables Claude Code
integration in our repository.

### What is Claude Code?

[Claude Code](https://claude.ai/code) is an AI coding agent that can
help with:
- Bug fixes and improvements  
- Documentation updates
- Implementing new features
- Code reviews and suggestions
- Writing tests
- And more!

### How it works

Once this PR is merged, we'll be able to interact with Claude by
mentioning @claude in a pull request or issue comment.
Once the workflow is triggered, Claude will analyze the comment and
surrounding context, and execute on the request in a GitHub action.

### Important Notes

- **This workflow won't take effect until this PR is merged**
- **@claude mentions won't work until after the merge is complete**
- The workflow runs automatically whenever Claude is mentioned in PR or
issue comments
- Claude gets access to the entire PR or issue context including files,
diffs, and previous comments

### Security

- Our Anthropic API key is securely stored as a GitHub Actions secret
- Only users with write access to the repository can trigger the
workflow
- All Claude runs are stored in the GitHub Actions run history
- Claude's default tools are limited to reading/writing files and
interacting with our repo by creating comments, branches, and commits.
- We can add more allowed tools by adding them to the workflow file
like:

```
allowed_tools: Bash(npm install),Bash(npm run build),Bash(npm run lint),Bash(npm run test)
```

There's more information in the [Claude Code action
repo](https://github.com/anthropics/claude-code-action).

After merging this PR, let's try mentioning @claude in a comment on any
PR to get started!

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
  * Introduced automated code review using Claude AI for pull requests.
* Added Claude AI code assistance, triggered by comments containing
"@claude" in issues and pull requests.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-25 15:00:03 -04:00
Eli Bosley
86b6c4f85b fix: inject Tailwind CSS into client entry point (#1537)
Added a Vite plugin to automatically inject the Tailwind CSS import into
the `unraid-components.client.js` entry file, enhancing the integration
of Tailwind CSS within the application. This change improves the setup
for styling components consistently across the project.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added automated validation to ensure Tailwind CSS styles are correctly
included in the custom elements build output.

* **Chores**
* Updated the build process to include a CSS validation step after
manifest generation.
* Enhanced development build configuration to enable CSS source maps and
optimize Tailwind CSS injection into web components.
  * Extended CSS theme with new responsive breakpoint variables.
* Improved CSS class specificity in user profile, server state, and
update modal components for consistent styling.
* Removed redundant style blocks and global CSS imports from multiple
components to streamline styling and reduce duplication.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-23 15:30:57 -04:00
Pujit Mehrotra
45bd73698b fix(connect): omit extraneous fields during connect config validation (#1538)
Prevent extraneous fields from migrating to `connect.json` from
`myservers.cfg`
2025-07-23 13:55:35 -04:00
Pujit Mehrotra
fee7d4613e refactor: add & use ConfigFilePersister primitive (#1534)
Add `ConfigFilePersister<T>` to provide automatic JSON file persistence
for configs. It bridges the gap between in-memory configuration (via
`ConfigService`) and persistent file storage, with minimal developer
effort.

## Key Features

- **Reactive Persistence**: Automatically saves config changes to disk
when `ConfigService` updates
- **NestJS Integration**: Implements lifecycle hooks for proper
initialization and cleanup
- **Standalone Operations**: Provides direct file access via
`getFileHandler()` for non-reactive use cases
- **Change Detection**: Only writes to disk when configuration actually
changes (performance optimization)
- **Error Handling**: Includes logging and graceful error handling
throughout

## How to Implement

Extend the class and implement these required methods:

```typescript
@Injectable()
class MyConfigPersister extends ConfigFilePersister<MyConfigType> {
  constructor(configService: ConfigService) {
    super(configService);
  }

  // Required: JSON filename in config directory
  fileName(): string { 
    return "my-config.json"; 
  }

  // Required: ConfigService key for reactive updates
  configKey(): string { 
    return "myConfig"; 
  }

  // Required: Default values for new installations
  defaultConfig(): MyConfigType {
    return { enabled: false, timeout: 5000 };
  }

  // optionally, override validate() and/or migrateConfig()
}
```

## Lifecycle Behavior

- **Initialization** (`onModuleInit`): Loads config from disk → sets in
ConfigService → starts reactive subscription
- **Runtime**: Automatically persists to disk when ConfigService changes
(buffered every 25ms)
- **Shutdown** (`onModuleDestroy`): Final persistence and cleanup

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Summary by CodeRabbit

* **New Features**
* Introduced a unified, robust configuration file management system with
automatic migration, validation, and efficient persistence for plugins
and services.

* **Refactor**
* Centralized configuration persistence logic into a shared base class,
simplifying and standardizing config handling.
* Refactored multiple config persisters to extend the new base class,
removing redundant manual file and lifecycle management.
* Removed legacy config state management, persistence helpers, and
related modules, streamlining the codebase.
* Simplified test suites to focus on core functionality and error
handling.

* **Chores**
* Updated dependencies to support the new configuration management
system.
* Integrated the new API config module into plugin modules for
consistent configuration handling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-23 13:34:12 -04:00
Michael Datelle
b6acf50c0d refactor: update modals and color picker (#1494)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* The Welcome modal now automatically appears when visiting the
`/welcome` page.
* "Create a password" button in the Welcome modal is now disabled while
loading.

* **Refactor**
* Activation and Welcome modals now use a new Dialog component for
improved layout and styling.
* Theme and server selection components now use a simplified Select
dropdown with options passed as data for a cleaner interface.

* **Tests**
* Updated modal-related tests to use the new Dialog component and
improved mocking for more accurate and maintainable test coverage.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: mdatelle <mike@datelle.net>
2025-07-23 08:28:31 -04:00
renovate[bot]
8279531f2b fix(deps): pin dependency @nuxt/ui to 3.2.0 (#1532)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [@nuxt/ui](https://ui.nuxt.com)
([source](https://redirect.github.com/nuxt/ui)) | dependencies | pin |
[`^3.2.0` ->
`3.2.0`](https://renovatebot.com/diffs/npm/@nuxt%2fui/3.2.0/3.2.0) |

Add the preset `:preserveSemverRanges` to your config if you don't want
to pin your dependencies.

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/unraid/api).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS40MC4wIiwidXBkYXRlZEluVmVyIjoiNDEuNDAuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 14:41:25 -04:00
Eli Bosley
0a18b38008 fix: truncate log files when they take up more than 5mb of space (#1530)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Updated configuration to enable WAN access, set custom ports, and add
new fields such as version and sandbox mode.

* **Bug Fixes**
* Improved log rotation reliability by directly managing log file size
and truncation every 20 minutes, with enhanced error handling and
logging.

* **Chores**
* Removed legacy log rotation configuration files and related test cases
to streamline maintenance.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-22 14:40:39 -04:00
Eli Bosley
23b2b88461 fix: use async for primary file read/writes (#1531)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Improved application performance and responsiveness by converting all
synchronous file system operations to asynchronous ones throughout the
application.
* Enhanced reliability of file checks and file writing, ensuring
non-blocking behavior during configuration, notification handling, and
service operations.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-22 14:40:30 -04:00
Eli Bosley
f5352e3a26 fix: add missing breakpoints (#1535)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Style**
* Introduced new CSS custom properties for additional responsive
breakpoints, enhancing layout adaptability across a wider range of
screen sizes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-22 10:14:48 -04:00
Pujit Mehrotra
9dfdb8dce7 fix: make settings grid responsive (#1463)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Introduced a new SettingsGrid component for consistent and responsive
grid layouts.

* **Refactor**
* Updated settings-related layouts to use the new SettingsGrid
component, improving maintainability and visual consistency across the
interface.

* **Chores**
  * Removed an unused CSS breakpoint variable from global styles.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-22 09:21:35 -04:00
Michael Datelle
407585cd40 feat(web): install and configure nuxt ui (#1524)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Introduced a primary color palette and enhanced color theming for the
UI.
* Added and showcased new UI button variants with primary color styling
on the main page.
* Integrated the @nuxt/ui module to enable advanced UI components and
theming options.

* **Style**
* Updated keyframe animations in global styles for improved CSS
structure.
* Refined color variables and UI color states for both light and dark
modes.

* **Chores**
  * Added @nuxt/ui as a project dependency.
  * Centralized UI configuration for easier theming management.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: mdatelle <mike@datelle.net>
2025-07-21 17:14:41 -04:00
Pujit Mehrotra
05056e7ca1 fix(notifications): gracefully handle & mask invalid notifications (#1529)
prevents log explosion due to large, invalid notifications.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved handling of invalid or corrupted notifications by displaying
a warning message instead of causing errors or interruptions.
* Enhanced robustness in displaying notification timestamps by
gracefully handling invalid date formats.

* **Refactor**
* Improved internal date formatting for notifications, ensuring more
consistent and user-friendly display.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210811542761865
2025-07-21 15:28:05 -04:00
renovate[bot]
a74d935b56 fix(deps): pin dependencies (#1528)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [@tailwindcss/cli](https://tailwindcss.com)
([source](https://redirect.github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-cli))
| dependencies | pin | [`^4.1.11` ->
`4.1.11`](https://renovatebot.com/diffs/npm/@tailwindcss%2fcli/4.1.11/4.1.11)
|
| [@tailwindcss/vite](https://tailwindcss.com)
([source](https://redirect.github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite))
| devDependencies | pin | [`^4.1.11` ->
`4.1.11`](https://renovatebot.com/diffs/npm/@tailwindcss%2fvite/4.1.11/4.1.11)
|
|
[tw-animate-css](https://redirect.github.com/Wombosvideo/tw-animate-css)
| dependencies | pin | [`^1.3.5` ->
`1.3.5`](https://renovatebot.com/diffs/npm/tw-animate-css/1.3.5/1.3.5) |

Add the preset `:preserveSemverRanges` to your config if you don't want
to pin your dependencies.

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/unraid/api).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS40MC4wIiwidXBkYXRlZEluVmVyIjoiNDEuNDAuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 13:13:23 -04:00
Eli Bosley
2c62e0ad09 feat: tailwind v4 (#1522)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Streamlined Tailwind CSS integration using Vite plugin, eliminating
the need for separate Tailwind config files.
* Updated theme and color variables for improved consistency and
maintainability.

* **Style**
* Standardized spacing, sizing, and font classes across all components
using Tailwind’s default scale.
* Reduced excessive gaps, padding, and font sizes for a more compact and
cohesive UI.
* Updated gradient, border, and shadow classes to match Tailwind v4
conventions.
* Replaced custom pixel-based classes with Tailwind’s bracketed
arbitrary value syntax where needed.
* Replaced focus outline styles from `outline-none` to `outline-hidden`
for consistent focus handling.
* Updated flex shrink/grow utility classes to use newer shorthand forms.
* Converted several component templates to use self-closing tags for
cleaner markup.
  * Adjusted icon sizes and spacing for improved visual balance.

* **Chores**
* Removed legacy Tailwind/PostCSS configuration files and related
scripts.
* Updated and cleaned up package dependencies for Tailwind v4 and
related plugins.
  * Removed unused or redundant build scripts and configuration exports.
  * Updated documentation to reflect new Tailwind v4 usage.
  * Removed Prettier Tailwind plugin from formatting configurations.
* Removed Nuxt Tailwind module in favor of direct Vite plugin
integration.
  * Cleaned up ESLint config by removing Prettier integration.

* **Bug Fixes**
  * Corrected invalid or outdated Tailwind class names and syntax.
* Fixed issues with max-width and other utility classes for improved
layout consistency.

* **Tests**
* Updated test assertions to match new class names and styling
conventions.

* **Documentation**
* Revised README and internal notes to clarify Tailwind v4 adoption and
configuration changes.
* Added new development notes emphasizing Tailwind v4 usage and
documentation references.

* **UI Components**
* Enhanced BrandButton stories with detailed variant, size, and padding
showcases for better visual testing.
* Improved theme store to apply dark mode class on both `<html>` and
`<body>` elements for compatibility.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-21 09:58:02 -04:00
renovate[bot]
1a8da6d92b fix(deps): update all non-major dependencies (#1510)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
| [@eslint/js](https://eslint.org)
([source](https://redirect.github.com/eslint/eslint/tree/HEAD/packages/js))
| [`9.30.1` ->
`9.31.0`](https://renovatebot.com/diffs/npm/@eslint%2fjs/9.30.1/9.31.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@eslint%2fjs/9.31.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@eslint%2fjs/9.30.1/9.31.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [@rollup/rollup-linux-x64-gnu](https://rollupjs.org/)
([source](https://redirect.github.com/rollup/rollup)) | [`4.44.2` ->
`4.45.1`](https://renovatebot.com/diffs/npm/@rollup%2frollup-linux-x64-gnu/4.44.2/4.45.1)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@rollup%2frollup-linux-x64-gnu/4.45.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@rollup%2frollup-linux-x64-gnu/4.44.2/4.45.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[@storybook/addon-docs](https://redirect.github.com/storybookjs/storybook/tree/next/code/addons/docs)
([source](https://redirect.github.com/storybookjs/storybook/tree/HEAD/code/addons/docs))
| [`9.0.16` ->
`9.0.17`](https://renovatebot.com/diffs/npm/@storybook%2faddon-docs/9.0.16/9.0.17)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@storybook%2faddon-docs/9.0.17?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@storybook%2faddon-docs/9.0.16/9.0.17?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[@storybook/addon-links](https://redirect.github.com/storybookjs/storybook/tree/next/code/addons/links)
([source](https://redirect.github.com/storybookjs/storybook/tree/HEAD/code/addons/links))
| [`9.0.16` ->
`9.0.17`](https://renovatebot.com/diffs/npm/@storybook%2faddon-links/9.0.16/9.0.17)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@storybook%2faddon-links/9.0.17?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@storybook%2faddon-links/9.0.16/9.0.17?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[@storybook/builder-vite](https://redirect.github.com/storybookjs/storybook/tree/next/code/builders/builder-vite/#readme)
([source](https://redirect.github.com/storybookjs/storybook/tree/HEAD/code/builders/builder-vite))
| [`9.0.16` ->
`9.0.17`](https://renovatebot.com/diffs/npm/@storybook%2fbuilder-vite/9.0.16/9.0.17)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@storybook%2fbuilder-vite/9.0.17?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@storybook%2fbuilder-vite/9.0.16/9.0.17?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[@storybook/vue3-vite](https://redirect.github.com/storybookjs/storybook/tree/next/code/frameworks/vue3-vite)
([source](https://redirect.github.com/storybookjs/storybook/tree/HEAD/code/frameworks/vue3-vite))
| [`9.0.16` ->
`9.0.17`](https://renovatebot.com/diffs/npm/@storybook%2fvue3-vite/9.0.16/9.0.17)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@storybook%2fvue3-vite/9.0.17?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@storybook%2fvue3-vite/9.0.16/9.0.17?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [@swc/core](https://swc.rs)
([source](https://redirect.github.com/swc-project/swc)) | [`1.12.11` ->
`1.12.14`](https://renovatebot.com/diffs/npm/@swc%2fcore/1.12.11/1.12.14)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@swc%2fcore/1.12.14?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@swc%2fcore/1.12.11/1.12.14?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[@types/node](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node)
([source](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node))
| [`22.16.3` ->
`22.16.4`](https://renovatebot.com/diffs/npm/@types%2fnode/22.16.3/22.16.4)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@types%2fnode/22.16.4?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@types%2fnode/22.16.3/22.16.4?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[@typescript-eslint/eslint-plugin](https://typescript-eslint.io/packages/eslint-plugin)
([source](https://redirect.github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin))
| [`8.36.0` ->
`8.37.0`](https://renovatebot.com/diffs/npm/@typescript-eslint%2feslint-plugin/8.36.0/8.37.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@typescript-eslint%2feslint-plugin/8.37.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@typescript-eslint%2feslint-plugin/8.36.0/8.37.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [cron](https://redirect.github.com/kelektiv/node-cron) | [`4.3.1` ->
`4.3.2`](https://renovatebot.com/diffs/npm/cron/4.3.1/4.3.2) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/cron/4.3.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/cron/4.3.1/4.3.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [eslint](https://eslint.org)
([source](https://redirect.github.com/eslint/eslint)) | [`9.30.1` ->
`9.31.0`](https://renovatebot.com/diffs/npm/eslint/9.30.1/9.31.0) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/eslint/9.31.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/eslint/9.30.1/9.31.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[eslint-plugin-storybook](https://redirect.github.com/storybookjs/storybook/code/lib/eslint-plugin#readme)
([source](https://redirect.github.com/storybookjs/storybook/tree/HEAD/code/lib/eslint-plugin))
| [`9.0.16` ->
`9.0.17`](https://renovatebot.com/diffs/npm/eslint-plugin-storybook/9.0.16/9.0.17)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/eslint-plugin-storybook/9.0.17?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/eslint-plugin-storybook/9.0.16/9.0.17?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [graphql-ws](https://the-guild.dev/graphql/ws)
([source](https://redirect.github.com/enisdenjo/graphql-ws)) | [`6.0.5`
-> `6.0.6`](https://renovatebot.com/diffs/npm/graphql-ws/6.0.5/6.0.6) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/graphql-ws/6.0.6?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/graphql-ws/6.0.5/6.0.6?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [nuxt](https://nuxt.com)
([source](https://redirect.github.com/nuxt/nuxt/tree/HEAD/packages/nuxt))
| [`3.17.6` ->
`3.17.7`](https://renovatebot.com/diffs/npm/nuxt/3.17.6/3.17.7) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/nuxt/3.17.7?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/nuxt/3.17.6/3.17.7?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [storybook](https://storybook.js.org)
([source](https://redirect.github.com/storybookjs/storybook/tree/HEAD/code/core))
| [`9.0.16` ->
`9.0.17`](https://renovatebot.com/diffs/npm/storybook/9.0.16/9.0.17) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/storybook/9.0.17?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/storybook/9.0.16/9.0.17?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[typescript-eslint](https://typescript-eslint.io/packages/typescript-eslint)
([source](https://redirect.github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint))
| [`8.36.0` ->
`8.37.0`](https://renovatebot.com/diffs/npm/typescript-eslint/8.36.0/8.37.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/typescript-eslint/8.37.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/typescript-eslint/8.36.0/8.37.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [zx](https://google.github.io/zx/)
([source](https://redirect.github.com/google/zx)) | [`8.6.2` ->
`8.7.1`](https://renovatebot.com/diffs/npm/zx/8.3.2/8.7.1) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/zx/8.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/zx/8.3.2/8.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [zx](https://google.github.io/zx/)
([source](https://redirect.github.com/google/zx)) | [`8.6.2` ->
`8.7.1`](https://renovatebot.com/diffs/npm/zx/8.6.2/8.7.1) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/zx/8.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/zx/8.6.2/8.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>eslint/eslint (@&#8203;eslint/js)</summary>

###
[`v9.31.0`](https://redirect.github.com/eslint/eslint/compare/v9.30.1...d5054e5454a537e9ade238c768c262c6c592cbc1)

[Compare
Source](https://redirect.github.com/eslint/eslint/compare/v9.30.1...v9.31.0)

</details>

<details>
<summary>rollup/rollup (@&#8203;rollup/rollup-linux-x64-gnu)</summary>

###
[`v4.45.1`](https://redirect.github.com/rollup/rollup/blob/HEAD/CHANGELOG.md#4451)

[Compare
Source](https://redirect.github.com/rollup/rollup/compare/v4.45.0...v4.45.1)

*2025-07-15*

##### Bug Fixes

- Resolve crash when using certain conditional expressions
([#&#8203;6009](https://redirect.github.com/rollup/rollup/issues/6009))

##### Pull Requests

- [#&#8203;6009](https://redirect.github.com/rollup/rollup/pull/6009):
Add hasDeoptimizedCache flag for ConditionalExpression
([@&#8203;TrickyPi](https://redirect.github.com/TrickyPi))

###
[`v4.45.0`](https://redirect.github.com/rollup/rollup/blob/HEAD/CHANGELOG.md#4450)

[Compare
Source](https://redirect.github.com/rollup/rollup/compare/v4.44.2...v4.45.0)

*2025-07-12*

##### Features

- Improve tree-shaking when both branches of a conditional expression
return the same boolean value
([#&#8203;6000](https://redirect.github.com/rollup/rollup/issues/6000))
- In environments that support both CJS and ESM, prefer the ESM build of
Rollup
([#&#8203;6005](https://redirect.github.com/rollup/rollup/issues/6005))

##### Bug Fixes

- Ensure static blocks do not prevent tree-shaking if they access `this`
([#&#8203;6001](https://redirect.github.com/rollup/rollup/issues/6001))

##### Pull Requests

- [#&#8203;6000](https://redirect.github.com/rollup/rollup/pull/6000):
feat: improve get literal value for conditional expression
([@&#8203;ahabhgk](https://redirect.github.com/ahabhgk),
[@&#8203;lukastaegert](https://redirect.github.com/lukastaegert))
- [#&#8203;6001](https://redirect.github.com/rollup/rollup/pull/6001):
Correct the parent scope for static blocks
([@&#8203;TrickyPi](https://redirect.github.com/TrickyPi),
[@&#8203;lukastaegert](https://redirect.github.com/lukastaegert))
- [#&#8203;6005](https://redirect.github.com/rollup/rollup/pull/6005):
fix: export field order prefer esm
([@&#8203;DylanPiercey](https://redirect.github.com/DylanPiercey))

</details>

<details>
<summary>storybookjs/storybook (@&#8203;storybook/addon-docs)</summary>

###
[`v9.0.17`](https://redirect.github.com/storybookjs/storybook/compare/v9.0.16...06a11ce246b2e7a52d41e43420e37162c55133aa)

[Compare
Source](https://redirect.github.com/storybookjs/storybook/compare/v9.0.16...v9.0.17)

</details>

<details>
<summary>storybookjs/storybook (@&#8203;storybook/addon-links)</summary>

###
[`v9.0.17`](https://redirect.github.com/storybookjs/storybook/compare/v9.0.16...06a11ce246b2e7a52d41e43420e37162c55133aa)

[Compare
Source](https://redirect.github.com/storybookjs/storybook/compare/v9.0.16...v9.0.17)

</details>

<details>
<summary>storybookjs/storybook
(@&#8203;storybook/builder-vite)</summary>

###
[`v9.0.17`](https://redirect.github.com/storybookjs/storybook/blob/HEAD/CHANGELOG.md#9017)

[Compare
Source](https://redirect.github.com/storybookjs/storybook/compare/v9.0.16...v9.0.17)

- Addon Vitest: Fix support for plain `stories.tsx` files -
[#&#8203;32041](https://redirect.github.com/storybookjs/storybook/pull/32041),
thanks [@&#8203;ghengeveld](https://redirect.github.com/ghengeveld)!
- Onboarding: Intent survey -
[#&#8203;31944](https://redirect.github.com/storybookjs/storybook/pull/31944),
thanks [@&#8203;ghengeveld](https://redirect.github.com/ghengeveld)!
- UI: Fix text color for failing stories in sidebar -
[#&#8203;32042](https://redirect.github.com/storybookjs/storybook/pull/32042),
thanks [@&#8203;ghengeveld](https://redirect.github.com/ghengeveld)!

</details>

<details>
<summary>swc-project/swc (@&#8203;swc/core)</summary>

###
[`v1.12.14`](https://redirect.github.com/swc-project/swc/blob/HEAD/CHANGELOG.md#11214---2025-07-14)

[Compare
Source](https://redirect.github.com/swc-project/swc/compare/v1.12.11...v1.12.14)

##### Bug Fixes

- **(es/minifier)** Don't inline arrow when it contain `this`
([#&#8203;10825](https://redirect.github.com/swc-project/swc/issues/10825))
([8b43bb3](8b43bb35bc))

- **(es/parser)** Make `export` in NS to not affect file type
([#&#8203;10799](https://redirect.github.com/swc-project/swc/issues/10799))
([ae22033](ae22033dc4))

- **(es/parser)** Correctly check ambient context
([#&#8203;10802](https://redirect.github.com/swc-project/swc/issues/10802))
([f97ea03](f97ea03523))

##### Features

- **(es/parser)** Enable support for dynamic import with `defer` phase
([#&#8203;10845](https://redirect.github.com/swc-project/swc/issues/10845))
([097d29d](097d29d21c))

- **(plugin)** Remove `bytecheck` to make Wasm plugins backward
compatible
([#&#8203;10842](https://redirect.github.com/swc-project/swc/issues/10842))
([30ad808](30ad80809c))

##### Miscellaneous Tasks

- **(bindings)** Fix dependency issues
([7c57fbb](7c57fbb103))

- **(deps)** Update `class-validator` to avoid comments
([#&#8203;10819](https://redirect.github.com/swc-project/swc/issues/10819))
([bacfa4b](bacfa4b56d))

- **(ide)** Enable `--workspace` for rust-analyzer check
([#&#8203;10809](https://redirect.github.com/swc-project/swc/issues/10809))
([92647ff](92647ff9d9))

##### Performance

- **(es/minifier)** Use `u8` for `remaining_depth`
([#&#8203;10833](https://redirect.github.com/swc-project/swc/issues/10833))
([ed6956a](ed6956a46e))

- **(hstr)** Inline one more byte
([#&#8203;10817](https://redirect.github.com/swc-project/swc/issues/10817))
([3886c97](3886c9720d))

- **(hstr)** Remove static tag
([#&#8203;10832](https://redirect.github.com/swc-project/swc/issues/10832))
([66ae1e8](66ae1e8d5a))

##### Refactor

- **(es/helpers)** Make inline helpers optional at compile time
([#&#8203;10808](https://redirect.github.com/swc-project/swc/issues/10808))
([53f3881](53f38811cc))

- **(es/lexer)** Don't store buffer in lexer
([#&#8203;10830](https://redirect.github.com/swc-project/swc/issues/10830))
([cac40f1](cac40f135d))

- **(es/lints)** Remove warnings without features
([#&#8203;10794](https://redirect.github.com/swc-project/swc/issues/10794))
([41d507f](41d507fe1e))

- **(es/parser)** Reduce token query
([#&#8203;10834](https://redirect.github.com/swc-project/swc/issues/10834))
([5cd5185](5cd5185a7a))

- **(es/parser)** Reduce call to `parse_decorators`
([#&#8203;10846](https://redirect.github.com/swc-project/swc/issues/10846))
([356d3a0](356d3a0850))

- **(es/parser)** Remove duplicate check
([#&#8203;10847](https://redirect.github.com/swc-project/swc/issues/10847))
([2b04efd](2b04efd540))

- **(es/preset-env)** Use strpool,phf for corejs2 data
([#&#8203;10803](https://redirect.github.com/swc-project/swc/issues/10803))
([1652fd8](1652fd8038))

- **(es/react)** Remove redundant `replace` calls
([#&#8203;10795](https://redirect.github.com/swc-project/swc/issues/10795))
([a670b37](a670b37c33))

- **(es/react)** Remove `count_children`
([#&#8203;10818](https://redirect.github.com/swc-project/swc/issues/10818))
([2116ab2](2116ab2fa2))

- **(hstr)** Cleanup duplicate header
([#&#8203;10812](https://redirect.github.com/swc-project/swc/issues/10812))
([630dde9](630dde93c9))

- **(hstr)** Make the deallocation of `Atom`s explicit
([#&#8203;10813](https://redirect.github.com/swc-project/swc/issues/10813))
([406433d](406433d55d))

- **(hstr)** Remove `is_global`
([#&#8203;10820](https://redirect.github.com/swc-project/swc/issues/10820))
([afda0f9](afda0f9d0d))

##### Testing

- **(es/plugin)** Test memory layout of archived types
([#&#8203;10841](https://redirect.github.com/swc-project/swc/issues/10841))
([502e991](502e991a8b))

</details>

<details>
<summary>typescript-eslint/typescript-eslint
(@&#8203;typescript-eslint/eslint-plugin)</summary>

###
[`v8.37.0`](https://redirect.github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/eslint-plugin/CHANGELOG.md#8370-2025-07-14)

[Compare
Source](https://redirect.github.com/typescript-eslint/typescript-eslint/compare/v8.36.0...v8.37.0)

##### 🩹 Fixes

- **eslint-plugin:** \[unified-signatures] fix false positives for
ignoreOverloadsWithDifferentJSDoc option
([#&#8203;11381](https://redirect.github.com/typescript-eslint/typescript-eslint/pull/11381))

##### ❤️ Thank You

- Yukihiro Hasegawa [@&#8203;y-hsgw](https://redirect.github.com/y-hsgw)

You can read about our [versioning
strategy](https://main--typescript-eslint.netlify.app/users/versioning)
and
[releases](https://main--typescript-eslint.netlify.app/users/releases)
on our website.

</details>

<details>
<summary>kelektiv/node-cron (cron)</summary>

###
[`v4.3.2`](https://redirect.github.com/kelektiv/node-cron/blob/HEAD/CHANGELOG.md#432-2025-07-13)

[Compare
Source](https://redirect.github.com/kelektiv/node-cron/compare/v4.3.1...v4.3.2)

##### 🛠 Builds

- **deps:** update dependency luxon to ~3.7.0
([db69c74](db69c74501))

##### ♻️ Chores

- **action:** update github/codeql-action action to v3.29.0
([#&#8203;990](https://redirect.github.com/kelektiv/node-cron/issues/990))
([a3fbb3c](a3fbb3cc4d))
- **action:** update github/codeql-action action to v3.29.2
([0403c53](0403c53320))
- **action:** update marocchino/sticky-pull-request-comment action to
v2.9.3
([eda0c4d](eda0c4df35))
- **action:** update ossf/scorecard-action action to v2.4.2
([#&#8203;991](https://redirect.github.com/kelektiv/node-cron/issues/991))
([29a3a60](29a3a604ef))
- **action:** update step-security/harden-runner action to v2.12.1
([ba49a56](ba49a5656c))
- **action:** update step-security/harden-runner action to v2.12.2
([845202e](845202ee97))
- **deps:** lock file maintenance
([#&#8203;989](https://redirect.github.com/kelektiv/node-cron/issues/989))
([bc1bf72](bc1bf72ff7))
- **deps:** lock file maintenance
([#&#8203;999](https://redirect.github.com/kelektiv/node-cron/issues/999))
([e78d986](e78d9869d6))
- **deps:** update dependency
[@&#8203;swc](https://redirect.github.com/swc)/core to v1.12.1
([#&#8203;992](https://redirect.github.com/kelektiv/node-cron/issues/992))
([b5d3bd3](b5d3bd3328))
- **deps:** update dependency
[@&#8203;swc](https://redirect.github.com/swc)/core to v1.12.5
([d374494](d374494609))
- **deps:** update dependency
[@&#8203;swc](https://redirect.github.com/swc)/core to v1.12.9
([8060c41](8060c41685))
- **deps:** update dependency
[@&#8203;types](https://redirect.github.com/types)/node to v22.15.32
([#&#8203;993](https://redirect.github.com/kelektiv/node-cron/issues/993))
([ce9743b](ce9743ba05))
- **deps:** update dependency
[@&#8203;types](https://redirect.github.com/types)/node to v22.16.0
([7bae5b1](7bae5b1ef8))
- **deps:** update linters
([24eb53f](24eb53ff67))
- **deps:** update linters
([#&#8203;995](https://redirect.github.com/kelektiv/node-cron/issues/995))
([9395484](9395484758))
- **deps:** update node.js to v23.11.1
([#&#8203;985](https://redirect.github.com/kelektiv/node-cron/issues/985))
([674a344](674a3448b5))
- **deps:** update semantic-release related packages
([cc2676a](cc2676aa88))
- **deps:** update semantic-release related packages
([#&#8203;994](https://redirect.github.com/kelektiv/node-cron/issues/994))
([4d738df](4d738df05f))

</details>

<details>
<summary>eslint/eslint (eslint)</summary>

###
[`v9.31.0`](https://redirect.github.com/eslint/eslint/compare/v9.30.1...14053edc64bd378ab920575f2488fbfcbb5a4ea0)

[Compare
Source](https://redirect.github.com/eslint/eslint/compare/v9.30.1...v9.31.0)

</details>

<details>
<summary>enisdenjo/graphql-ws (graphql-ws)</summary>

###
[`v6.0.6`](https://redirect.github.com/enisdenjo/graphql-ws/blob/HEAD/CHANGELOG.md#606)

[Compare
Source](https://redirect.github.com/enisdenjo/graphql-ws/compare/v6.0.5...v6.0.6)

##### Patch Changes

-
[#&#8203;648](https://redirect.github.com/enisdenjo/graphql-ws/pull/648)
[`1f53bb4`](1f53bb48b1)
Thanks [@&#8203;enisdenjo](https://redirect.github.com/enisdenjo)! - Fix
building issues causing CJS type definitions referencing ESM modules

</details>

<details>
<summary>nuxt/nuxt (nuxt)</summary>

###
[`v3.17.7`](https://redirect.github.com/nuxt/nuxt/releases/tag/v3.17.7)

[Compare
Source](https://redirect.github.com/nuxt/nuxt/compare/v3.17.6...v3.17.7)

> 3.17.7 is the last patch release before v3.18.

#####  Upgrading

Our recommendation for upgrading is to run:

```sh
npx nuxt upgrade --dedupe
```

This will deduplicate your lockfile as well, and help ensure that you
pull in updates from other dependencies that Nuxt relies on,
particularly in the unjs ecosystem.

##### 👉 Changelog

[compare
changes](https://redirect.github.com/nuxt/nuxt/compare/v3.17.6...v3.17.7)

##### 🩹 Fixes

- **nuxt:** Safe-guard `extraPageMetaExtractionKeys`
([#&#8203;32510](https://redirect.github.com/nuxt/nuxt/pull/32510))
- **nuxt:** Expose `loadBuilder` error cause
([8f13ce3c2](https://redirect.github.com/nuxt/nuxt/commit/8f13ce3c2))
- **vite:** Handle resolving string vite input
([#&#8203;32527](https://redirect.github.com/nuxt/nuxt/pull/32527))
- **nuxt:** Wrap only server components with island generic
([#&#8203;32540](https://redirect.github.com/nuxt/nuxt/pull/32540))
- **vite:** Ignore when client entry cannot be resolved
([19a292f34](https://redirect.github.com/nuxt/nuxt/commit/19a292f34))
- **nuxt:** Normalize segment catchall pattern before checking for
parent
([#&#8203;32413](https://redirect.github.com/nuxt/nuxt/pull/32413))
- **nuxt:** Update warning message to warn against `null` values
([c1b83eab5](https://redirect.github.com/nuxt/nuxt/commit/c1b83eab5))
- **nuxt:** Ensure `semver.satisfies` returns true for pre-release
versions
([#&#8203;32574](https://redirect.github.com/nuxt/nuxt/pull/32574))
- **nuxt:** Scroll to anchor if present when changing page without saved
position
([#&#8203;32376](https://redirect.github.com/nuxt/nuxt/pull/32376))
- **nuxt:** Handle `execute being passed to `watch\`
([#&#8203;32591](https://redirect.github.com/nuxt/nuxt/pull/32591))

##### 📖 Documentation

- Update fetch types
([#&#8203;32522](https://redirect.github.com/nuxt/nuxt/pull/32522))
- Clarify that runtime env variables must start with `NUXT_`
([#&#8203;32223](https://redirect.github.com/nuxt/nuxt/pull/32223))
- Fix key change behavior in `useAsyncData` and `useFetch` migration
([#&#8203;32560](https://redirect.github.com/nuxt/nuxt/pull/32560))
- Change return type of async data from `undefined` to `null` in v3 docs
([#&#8203;32562](https://redirect.github.com/nuxt/nuxt/pull/32562))
- Add section on custom hooks for Nuxt modules
([#&#8203;32586](https://redirect.github.com/nuxt/nuxt/pull/32586))
- Provide `async` keyword
([#&#8203;32587](https://redirect.github.com/nuxt/nuxt/pull/32587))
- Move augmenting hook types in hooks page
([#&#8203;32595](https://redirect.github.com/nuxt/nuxt/pull/32595))
- Add section about module loading order
([#&#8203;32597](https://redirect.github.com/nuxt/nuxt/pull/32597))

#####  Tests

- Reenable skipped unit tests
([8fc9b9ee9](https://redirect.github.com/nuxt/nuxt/commit/8fc9b9ee9))
- Update test snapshot for `generateTypes`
([c0855439d](https://redirect.github.com/nuxt/nuxt/commit/c0855439d))
- Improve page scanning test stability
([84b96f3de](https://redirect.github.com/nuxt/nuxt/commit/84b96f3de))
- Pass timeZone in to `<NuxtTime>` test
([#&#8203;32558](https://redirect.github.com/nuxt/nuxt/pull/32558))
- Add more useAsyncData + useFetch tests
([#&#8203;32585](https://redirect.github.com/nuxt/nuxt/pull/32585))
- Avoid hard-coding async-data keys
([bfca95118](https://redirect.github.com/nuxt/nuxt/commit/bfca95118))

##### ❤️ Contributors

- Daniel Roe
([@&#8203;danielroe](https://redirect.github.com/danielroe))
- Julien Huang
([@&#8203;huang-julien](https://redirect.github.com/huang-julien))
- abeer0 ([@&#8203;iiio2](https://redirect.github.com/iiio2))
- Bobbie Goede
([@&#8203;BobbieGoede](https://redirect.github.com/BobbieGoede))
- Damian Głowala
([@&#8203;DamianGlowala](https://redirect.github.com/DamianGlowala))
- Nestor Vera ([@&#8203;hacknug](https://redirect.github.com/hacknug))
- Ezra Ashenafi ([@&#8203;Eazash](https://redirect.github.com/Eazash))
- Mike Laumann Bellika
([@&#8203;MikeBellika](https://redirect.github.com/MikeBellika))
- Maxime Pauvert
([@&#8203;maximepvrt](https://redirect.github.com/maximepvrt))
- Chriest Yu ([@&#8203;jcppman](https://redirect.github.com/jcppman))
- Andrei Hudalla
([@&#8203;paranoidPhantom](https://redirect.github.com/paranoidPhantom))
- Sigrid Huemer ([@&#8203;s1gr1d](https://redirect.github.com/s1gr1d))
- xjccc ([@&#8203;xjccc](https://redirect.github.com/xjccc))

</details>

<details>
<summary>storybookjs/storybook (storybook)</summary>

###
[`v9.0.17`](https://redirect.github.com/storybookjs/storybook/compare/v9.0.16...06a11ce246b2e7a52d41e43420e37162c55133aa)

[Compare
Source](https://redirect.github.com/storybookjs/storybook/compare/v9.0.16...v9.0.17)

</details>

<details>
<summary>typescript-eslint/typescript-eslint
(typescript-eslint)</summary>

###
[`v8.37.0`](https://redirect.github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/typescript-eslint/CHANGELOG.md#8370-2025-07-14)

[Compare
Source](https://redirect.github.com/typescript-eslint/typescript-eslint/compare/v8.36.0...v8.37.0)

##### 🚀 Features

- **typescript-estree:** infer tsconfigRootDir from call stack
([#&#8203;11370](https://redirect.github.com/typescript-eslint/typescript-eslint/pull/11370))

##### ❤️ Thank You

- Josh Goldberg 

You can read about our [versioning
strategy](https://main--typescript-eslint.netlify.app/users/versioning)
and
[releases](https://main--typescript-eslint.netlify.app/users/releases)
on our website.

</details>

<details>
<summary>google/zx (zx)</summary>

###
[`v8.7.1`](https://redirect.github.com/google/zx/releases/tag/8.7.1): —
Pipe Whisperer

[Compare
Source](https://redirect.github.com/google/zx/compare/8.7.0...8.7.1)

Continues
[v8.7.0](https://redirect.github.com/google/zx/releases/tag/8.7.0):
handles new `ps()` corner case and improves `$.kill` mechanics on
Windows
[#&#8203;1266](https://redirect.github.com/google/zx/issues/1266)
[#&#8203;1267](https://redirect.github.com/google/zx/pull/1267)
[#&#8203;1269](https://redirect.github.com/google/zx/pull/1269)
[webpod/ps#14](https://redirect.github.com/webpod/ps/pull/14)

###
[`v8.7.0`](https://redirect.github.com/google/zx/releases/tag/8.7.0): —
Solder Savior

[Compare
Source](https://redirect.github.com/google/zx/compare/8.6.2...8.7.0)

Important fixes for annoying flaky bugs

#### kill() 🐞

We've found an interesting case
[#&#8203;1262](https://redirect.github.com/google/zx/pull/1262)

```js
const p = $`sleep 1000`
const {pid} = p // 12345
await p.kill()
```

If we kill the process again, the result might be unexpected:

```js
await ps({pid}) // {pid: 12345, ppid: 67890, command: 'another command', ...}
p.kill()
```

This happens because the `pid` may be reused by the system for another
process, so we've added extra assertions to prevent indeterminacy:

```js
p.kill()  // Error: Too late to kill the process.
p.abort() // Error: Too late to abort the process.
```

#### ps() 🐛

- `ps()` uses
**[wmic](https://en.wikipedia.org/wiki/Windows_Management_Instrumentation)**
internally on Windows, it relies on *fragile* heuristics to parse the
output. We have improved this logic to handle more format variants, but
over time (in v9 maybe) we're planning to change the approach.

[#&#8203;1256](https://redirect.github.com/google/zx/pull/1256)
[#&#8203;1263](https://redirect.github.com/google/zx/issues/1263)
[webpod/ps#12](https://redirect.github.com/webpod/ps/pull/12)
[webpod/ingrid#6](https://redirect.github.com/webpod/ingrid/pull/6)

```js
const [root] = await ps.lookup({ pid: process.pid })
assert.equal(root.pid, process.pid)
```

###
[`v8.6.2`](https://redirect.github.com/google/zx/releases/tag/8.6.2): —
Flow Unstoppable

[Compare
Source](https://redirect.github.com/google/zx/compare/8.6.1...8.6.2)

Fixes `$.prefix` & `$.postfix` values settings via env variables
[#&#8203;1261](https://redirect.github.com/google/zx/pull/1261)
[#&#8203;1260](https://redirect.github.com/google/zx/issues/1260)

###
[`v8.6.1`](https://redirect.github.com/google/zx/releases/tag/8.6.1): —
Drain Hero

[Compare
Source](https://redirect.github.com/google/zx/compare/8.6.0...8.6.1)

- Use `process.env.SHELL` as default shell if defined
[#&#8203;1252](https://redirect.github.com/google/zx/pull/1252)

```bash
SHELL=/bin/zsh zx script.js
```

- Accept numeric strings as `parseDuration()` arg
[#&#8203;1249](https://redirect.github.com/google/zx/pull/1249)

```js
await sleep(1000)   // 1 second
await sleep('1000') // 1 second
```

- Update docker base image to `node:24-alpine`
[#&#8203;1239](https://redirect.github.com/google/zx/pull/1239)
- Docs improvements
[#&#8203;1242](https://redirect.github.com/google/zx/pull/1242)
[#&#8203;1243](https://redirect.github.com/google/zx/pull/1243)
[#&#8203;1246](https://redirect.github.com/google/zx/pull/1246)
[#&#8203;1248](https://redirect.github.com/google/zx/pull/1248)
[#&#8203;1251](https://redirect.github.com/google/zx/pull/1251)

###
[`v8.6.0`](https://redirect.github.com/google/zx/releases/tag/8.6.0): —
Valve Vanguard

[Compare
Source](https://redirect.github.com/google/zx/compare/8.5.5...8.6.0)

- Enabled `thenable` params processing for `$` literals
[#&#8203;1237](https://redirect.github.com/google/zx/pull/1237)

```js
const a1 = $`echo foo`
const a2 = new Promise((resolve) => setTimeout(resolve, 20, ['bar', 'baz']))

await $`echo ${a1} ${a2}` // foo bar baz
```

- A dozen of internal refactorings
[#&#8203;1225](https://redirect.github.com/google/zx/pull/1225)
[#&#8203;1226](https://redirect.github.com/google/zx/pull/1226)
[#&#8203;1228](https://redirect.github.com/google/zx/pull/1228)
[#&#8203;1229](https://redirect.github.com/google/zx/pull/1229)
[#&#8203;1230](https://redirect.github.com/google/zx/pull/1230)
[#&#8203;1231](https://redirect.github.com/google/zx/pull/1231)
[#&#8203;1232](https://redirect.github.com/google/zx/pull/1232)
[#&#8203;1233](https://redirect.github.com/google/zx/pull/1233)
[#&#8203;1234](https://redirect.github.com/google/zx/pull/1234)
[#&#8203;1235](https://redirect.github.com/google/zx/pull/1235)
[#&#8203;1236](https://redirect.github.com/google/zx/pull/1236)
[#&#8203;1238](https://redirect.github.com/google/zx/pull/1238)
[#&#8203;1239](https://redirect.github.com/google/zx/pull/1239)
  - Deps bumping
  - Bytes shrinking
  - Docs improvements

###
[`v8.5.5`](https://redirect.github.com/google/zx/releases/tag/8.5.5): —
PVC Wizard

[Compare
Source](https://redirect.github.com/google/zx/compare/8.5.4...8.5.5)

Minor feature polish.

- `ProcessPromise` and `ProcessOutput` `lines()` getters now accept a
custom delimiter
[#&#8203;1220](https://redirect.github.com/google/zx/issues/1220)
[#&#8203;1218](https://redirect.github.com/google/zx/issues/1218)

```ts
const cwd = tempdir()
const delimiter = '\0'

const p1 = $({
  cwd
})`touch foo bar baz; find ./ -type f -print0 -maxdepth 1`
(await p1.lines(delimiter)).sort() // ['./bar', './baz', './foo']
  
// or via options
const lines = []
const p2 = $({
  delimiter,
  cwd,
})`find ./ -type f -print0 -maxdepth 1`

for await (const line of p2) {
  lines.push(line)
}

lines.sort() // ['./bar', './baz', './foo']
```

- Handle `.nothrow()` option in `ProcessProcess[AsyncIterator]`
[#&#8203;1216](https://redirect.github.com/google/zx/pull/1216)
[#&#8203;1217](https://redirect.github.com/google/zx/pull/1217)
- Updates yaml to
[v2.8.0](https://redirect.github.com/eemeli/yaml/releases/tag/v2.8.0)
[#&#8203;1221](https://redirect.github.com/google/zx/pull/1221)

###
[`v8.5.4`](https://redirect.github.com/google/zx/releases/tag/8.5.4): —
Pipe Dreamer

[Compare
Source](https://redirect.github.com/google/zx/compare/8.5.3...8.5.4)

- Fixed the `pipe(file: string)` signature type declaration
[#&#8203;1208](https://redirect.github.com/google/zx/issues/1208)
[#&#8203;1209](https://redirect.github.com/google/zx/issues/1209)

###
[`v8.5.3`](https://redirect.github.com/google/zx/releases/tag/8.5.3): —
Trap Master

[Compare
Source](https://redirect.github.com/google/zx/compare/8.5.2...8.5.3)

- Another portion of JSR related improvements
[#&#8203;1193](https://redirect.github.com/google/zx/pull/1193)
[#&#8203;1192](https://redirect.github.com/google/zx/pull/1192)
- Goods refactoring
[#&#8203;1195](https://redirect.github.com/google/zx/pull/1195)
  - Fixes `expBackoff` implementation
  - Sets `$.log.output` as default `spinner()` output
  - Makes configurable `question()` I/O
- Added
[Graaljs](https://www.graalvm.org/latest/reference-manual/js/NodeJS/)
compatability test
[#&#8203;1194](https://redirect.github.com/google/zx/pull/1194)
- Docs improvements, usage examples updates
[#&#8203;1198](https://redirect.github.com/google/zx/pull/1198)

###
[`v8.5.2`](https://redirect.github.com/google/zx/releases/tag/8.5.2): —
Threaded Perfection

[Compare
Source](https://redirect.github.com/google/zx/compare/8.5.0...8.5.2)

- Various JSR fixes
[#&#8203;1189](https://redirect.github.com/google/zx/pull/1189)
[#&#8203;1186](https://redirect.github.com/google/zx/pull/1186)
[#&#8203;1179](https://redirect.github.com/google/zx/pull/1179)
[#&#8203;1187](https://redirect.github.com/google/zx/pull/1187)
- Docs improvements
[#&#8203;1185](https://redirect.github.com/google/zx/pull/1185)
[#&#8203;1181](https://redirect.github.com/google/zx/pull/1181)

###
[`v8.5.0`](https://redirect.github.com/google/zx/releases/tag/8.5.0): —
Flow Splitter

[Compare
Source](9ba1fb4b8d...8.5.0)

In this release we're significantly expanding the zx setup capabilities.

#### zx@lite

Just core functions without extras, ~7x smaller than the full version.
[#&#8203;1131](https://redirect.github.com/google/zx/pull/1131)

```shell
npm i zx@lite
npm i zx@8.5.0-lite
```

Detailed comparison: [zx/versions](https://google.github.io/zx/versions)

```ts
import { $ } from 'zx'
await $`echo foo`
```

#### Channels

We have completely reforged the distribution flow. Now zx is available
in multiple formats:

- [npmjs](https://www.npmjs.com/package/zx)
- [GH npm](https://redirect.github.com/google/zx/pkgs/npm/zx)
- [GH repo](https://redirect.github.com/google/zx)
- [GH docker](https://redirect.github.com/google/zx/pkgs/container/zx)
- [JSR](https://jsr.io/@&#8203;webpod/zx)
-
[Homebrew](https://redirect.github.com/Homebrew/homebrew-core/blob/master/Formula/z/zx.rb)

```shell

### npm pkg from registry.npmjs.org
npm i zx        

### install directly from the GH
npm i google/zx 

### from GH the npm registry
npm i --registry=https://npm.pkg.github.com @&#8203;google/zx

### fetch from the JSR
### https://jsr.io/docs/using-packages

### @&#8203;webpod is temporary JSR scope until @&#8203;google/zx becomes ready, we'll migrate later
npx jsr add @&#8203;webpod/zx
deno add jsr:@&#8203;webpod/zx

### homebrew formula
### https://github.com/Homebrew/homebrew-core/blob/master/Formula/z/zx.rb
brew install zx
```

[#&#8203;1141](https://redirect.github.com/google/zx/pull/1141)...
[#&#8203;1157](https://redirect.github.com/google/zx/pull/1157)

#### Docker

If you'd prefer to run zx in a container, you can pull the image from
the GH docker registry.
[node:22-alpine](https://hub.docker.com/_/node) is used as a base.
[#&#8203;1142](https://redirect.github.com/google/zx/pull/1142)
[#&#8203;1145](https://redirect.github.com/google/zx/pull/1145)

```shell
docker pull ghcr.io/google/zx:8.5.0
docker run -t ghcr.io/google/zx:8.5.0 -e="await \$({verbose: true})\`echo foo\`"
docker run -t -i -v ./:/script ghcr.io/google/zx:8.5.0 script/t.js
```

#### Chores

- Introduced fetch pipe helper to bypass string size limits
[#&#8203;1130](https://redirect.github.com/google/zx/pull/1130)
[#&#8203;977](https://redirect.github.com/google/zx/issues/977)

```ts
const p1 = fetch('https://example.com').pipe($`cat`)
const p2 = fetch('https://example.com').pipe`cat`
```

- Added `glob.sync` shortcut
[#&#8203;1135](https://redirect.github.com/google/zx/pull/1135)

```ts
import { glob } from 'zx'
const packages = glob.sync(['package.json', 'packages/*/package.json'])
```

- Restored CLI flags and envars symmetry
[#&#8203;1137](https://redirect.github.com/google/zx/pull/1137)
[#&#8203;1138](https://redirect.github.com/google/zx/pull/1138)

```shell
ZX_REGISTRY='https://custom-registry.example.com' zx script.js
```

- Enhanced errors stacktrace formatting
[#&#8203;1166](https://redirect.github.com/google/zx/pull/1166)
- Improved 3rd party licenses digest
[#&#8203;1140](https://redirect.github.com/google/zx/pull/1140)
- Enabled zizmor checks
[#&#8203;1126](https://redirect.github.com/google/zx/pull/1126)
- Docs improvements
[#&#8203;1128](https://redirect.github.com/google/zx/pull/1128)
[#&#8203;1134](https://redirect.github.com/google/zx/pull/1134)
[#&#8203;1136](https://redirect.github.com/google/zx/pull/1136)
[#&#8203;1164](https://redirect.github.com/google/zx/pull/1164)

###
[`v8.4.2`](https://redirect.github.com/google/zx/compare/8.4.1...9ba1fb4b8d17a4f5f0067d438b49568606469903)

[Compare
Source](https://redirect.github.com/google/zx/compare/8.4.1...9ba1fb4b8d17a4f5f0067d438b49568606469903)

###
[`v8.4.1`](https://redirect.github.com/google/zx/releases/tag/8.4.1): –
Rusty Elbow

[Compare
Source](https://redirect.github.com/google/zx/compare/8.4.0...8.4.1)

Logger enhancements are arriving in this release.
[#&#8203;1119](https://redirect.github.com/google/zx/issues/1119)
[#&#8203;1122](https://redirect.github.com/google/zx/pull/1122)
[#&#8203;1123](https://redirect.github.com/google/zx/pull/1123)
[#&#8203;1125](https://redirect.github.com/google/zx/pull/1125)

- You can customize the output by defining your own formatters for each
log entry kind.

```ts
$.log.formatters = {
  cmd: (entry: LogEntry) => `CMD: ${entry.cmd}`,
  fetch: (entry: LogEntry) => `FETCH: ${entry.url}`
  //...
}
```

- Cmd highlighter now *should* properly detect bins and arguments. If
still not, please report it in
[#&#8203;1122](https://redirect.github.com/google/zx/pull/1122)
- Switched to TS 5.8
[#&#8203;1120](https://redirect.github.com/google/zx/pull/1120)
- Applied [zizmor](https://woodruffw.github.io/zizmor/) to check GHA
workflows
[#&#8203;1126](https://redirect.github.com/google/zx/pull/1126)
- Prettier is now enabled as a pre-commit hook
[#&#8203;1118](https://redirect.github.com/google/zx/pull/1118)

###
[`v8.4.0`](https://redirect.github.com/google/zx/releases/tag/8.4.0): –
Drip Detective

[Compare
Source](https://redirect.github.com/google/zx/compare/8.3.2...8.4.0)

Try the new batch of enhancements: `npm i zx@8.4.0`
https://www.npmjs.com/package/zx/v/8.4.0

#### Changes

- The CLI option `--prefer-local` now allows linking both external
binaries and packages
[#&#8203;1116](https://redirect.github.com/google/zx/pull/1116)
[#&#8203;1117](https://redirect.github.com/google/zx/pull/1117)

```js
const cwd = tmpdir()
const external = tmpdir()
await fs.outputJson(path.join(external, 'node_modules/a/package.json'), {
  name: 'a',
  version: '1.0.0',
  type: 'module',
  exports: './index.js',
})
await fs.outputFile(
  path.join(external, 'node_modules/a/index.js'),
  `
export const a = 'AAA'
`
)
const script = `
import {a} from 'a'
console.log(a);
`
const out = await $`zx --cwd=${cwd} --prefer-local=${external} --test <<< ${script}`
assert.equal(out.stdout, 'AAA\n')
```

- The `quote` has been slightly changed for a conner case, when zx
literal gets an array.
[#&#8203;999](https://redirect.github.com/google/zx/issues/999)
[#&#8203;1113](https://redirect.github.com/google/zx/issues/1113)

```js
const p = $({prefix: '', postfix: ''})`echo ${[1, '', '*', '2']}`

// before
p.cmd //  `echo 1  $'*' 2`) 

// after
p.cmd //  `echo 1 $'' $'*' 2`) 
```

- Provided support for custom script extensions via CLI
[#&#8203;1104](https://redirect.github.com/google/zx/pull/1104)
[#&#8203;1105](https://redirect.github.com/google/zx/pull/1105)

```bash
zx script.zx           # Unknown file extension "\.zx"
zx --ext=mjs script.zx # OK
```

- Enhanced `nothrow` option to suppress any errors
[#&#8203;1108](https://redirect.github.com/google/zx/pull/1108)
[#&#8203;1109](https://redirect.github.com/google/zx/pull/1109)

```js
const err = new Error('BrokenSpawn')
const o = await $({
  nothrow: true,
  spawn() {
    throw err
  },
})`echo foo`
o.ok       // false
o.exitCode // null
o.message  // BrokenSpawn...
o.cause    // err
```

- `@types/node` and `@types/fs-extra` deps replaced with triple-slash
typing refs
[#&#8203;1102](https://redirect.github.com/google/zx/pull/1102)
- Made `ProcessOutput` iterable
[#&#8203;1101](https://redirect.github.com/google/zx/pull/1101)
- Handle inappropriate `ProcessPromise` instantiation
[#&#8203;1097](https://redirect.github.com/google/zx/pull/1097)
[#&#8203;1098](https://redirect.github.com/google/zx/pull/1098)
- Pass origin error as `ProcessOuput` cause
[#&#8203;1110](https://redirect.github.com/google/zx/pull/1110)
- Separated build and release steps
[#&#8203;1106](https://redirect.github.com/google/zx/pull/1106)
- Internal improvements
- Introduced API bus
[#&#8203;1083](https://redirect.github.com/google/zx/pull/1083)
- Optimized `ProcessOutput` inners
[#&#8203;1096](https://redirect.github.com/google/zx/pull/1096)
[#&#8203;1095](https://redirect.github.com/google/zx/pull/1095)
- Pinned deps
[#&#8203;1099](https://redirect.github.com/google/zx/pull/1099)
[#&#8203;1100](https://redirect.github.com/google/zx/pull/1100)
- Switched to explicit `.ts` extensions for relative imports
[#&#8203;1111](https://redirect.github.com/google/zx/pull/1111)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/unraid/api).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4yMy4yIiwidXBkYXRlZEluVmVyIjoiNDEuMjMuMiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-15 14:55:56 -04:00
github-actions[bot]
81808ada0f chore(main): release 4.10.0 (#1505)
🤖 I have created a release *beep* *boop*
---


## [4.10.0](https://github.com/unraid/api/compare/v4.9.5...v4.10.0)
(2025-07-15)


### Features

* trial extension allowed within 5 days of expiration
([#1490](https://github.com/unraid/api/issues/1490))
([f34a33b](f34a33bc9f))


### Bug Fixes

* delay `nginx:reload` file mod effect by 10 seconds
([#1512](https://github.com/unraid/api/issues/1512))
([af33e99](af33e999a0))
* **deps:** update all non-major dependencies
([#1489](https://github.com/unraid/api/issues/1489))
([53b05eb](53b05ebe5e))
* ensure no crash if emhttp state configs are missing
([#1514](https://github.com/unraid/api/issues/1514))
([1a7d35d](1a7d35d3f6))
* **my.servers:** improve DNS resolution robustness for backup server
([#1518](https://github.com/unraid/api/issues/1518))
([eecd9b1](eecd9b1017))
* over-eager cloud query from web components
([#1506](https://github.com/unraid/api/issues/1506))
([074370c](074370c42c))
* replace myservers.cfg reads in UpdateFlashBackup.php
([#1517](https://github.com/unraid/api/issues/1517))
([441e180](441e1805c1))
* rm short-circuit in `rc.unraid-api` if plugin config dir is absent
([#1515](https://github.com/unraid/api/issues/1515))
([29dcb7d](29dcb7d0f0))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-15 14:32:44 -04:00
Eli Bosley
eecd9b1017 fix(my.servers): improve DNS resolution robustness for backup server (#1518)
Add multiple fallback methods for DNS resolution when checking
backup.unraid.net

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved reliability of DNS resolution checks for backup services,
reducing false error reports.
* Enhanced error messages to provide clearer guidance if DNS resolution
fails, including advice to check DNS settings in network configuration.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-15 11:33:21 -04:00
Pujit Mehrotra
441e1805c1 fix: replace myservers.cfg reads in UpdateFlashBackup.php (#1517)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added a new method for verifying user sign-in status using a dedicated
configuration handler.
* Introduced a class to manage connection configuration and status
checks.

* **Refactor**
* Updated logic for checking connection and registration status to use
new configuration handling methods for improved clarity and reliability.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-15 10:30:04 -04:00
Pujit Mehrotra
29dcb7d0f0 fix: rm short-circuit in rc.unraid-api if plugin config dir is absent (#1515)
This short-circuit causes any/all `rc.unraid-api` invocations to
immediately fail on fresh 7.2 images (because
`/boot/config/dynamix.my.servers` doesn't exist).

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Removed initial checks and setup for a plugin directory and default
environment file in the startup script.
* Simplified environment switching with streamlined commands and
improved error handling.
* Removed deprecated environment path references and updated related
tests.
* **Documentation**
* Added descriptive comments clarifying build and environment settings.
* **Tests**
* Updated test cases by removing assertions related to deprecated
environment paths.
* **Maintenance**
  * Updated timestamp fixtures for consistency.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-15 09:48:12 -04:00
Pujit Mehrotra
1a7d35d3f6 fix: ensure no crash if emhttp state configs are missing (#1514)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added new utility functions to improve file writing reliability by
ensuring parent directories exist before writing.
* Introduced a new watch command for easier development workflow in the
shared package.

* **Bug Fixes**
* Improved startup behavior by logging warnings for missing
configuration keys instead of crashing, allowing initialization to
proceed.

* **Chores**
* Updated configuration version number and reformatted plugin list for
clarity.
* Relocated certain GraphQL schema type and enum declarations without
changing their content.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210788779106748
2025-07-15 09:48:01 -04:00
Pujit Mehrotra
af33e999a0 fix: delay nginx:reload file mod effect by 10 seconds (#1512)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Enhancements**
  * Added logging to indicate when Nginx is successfully reloaded.
* Introduced a 10-second delay with a log message before triggering
Nginx reloads in file modification effects.

* **Style**
* Removed a startup message from the Unraid API service plugin
installation process.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Eli Bosley <ekbosley@gmail.com>
2025-07-14 11:12:20 -04:00
renovate[bot]
85a35804c1 chore(deps): pin dependencies (#1478)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [jiti](https://redirect.github.com/unjs/jiti) | devDependencies | pin
| [`^2.4.2` ->
`2.4.2`](https://renovatebot.com/diffs/npm/jiti/2.4.2/2.4.2) |
| [node](https://redirect.github.com/actions/node-versions) | uses-with
| pin | `22` -> `22.17.0` |
| [wrangler](https://redirect.github.com/cloudflare/workers-sdk)
([source](https://redirect.github.com/cloudflare/workers-sdk/tree/HEAD/packages/wrangler))
| devDependencies | pin | [`^4.0.0` ->
`4.24.3`](https://renovatebot.com/diffs/npm/wrangler/4.24.3/4.24.3) |
| [ws](https://redirect.github.com/websockets/ws) | peerDependencies |
pin | [`^8.18.0` ->
`8.18.3`](https://renovatebot.com/diffs/npm/ws/8.18.3/8.18.3) |

Add the preset `:preserveSemverRanges` to your config if you don't want
to pin your dependencies.

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/unraid/api).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4yMy4yIiwidXBkYXRlZEluVmVyIjoiNDEuMjMuMiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-14 11:11:46 -04:00
Eli Bosley
a35c8ff2f1 refactor(install): add debugging to install process
- Remove redundant log file handling and display errors directly to users
- Add debug information for troubleshooting installation issues

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved installation and verification scripts to display output and
error messages directly to the user, rather than writing to a log file.
* Enhanced error messages to provide clearer instructions when issues
occur during installation or verification.

* **New Features**
* Added detailed debug output during the API service startup to assist
with troubleshooting.

* **Chores**
* Updated script environments and streamlined directory creation for
improved reliability.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-14 10:59:05 -04:00
renovate[bot]
153e7a1e3a chore(deps): update dependency @vitejs/plugin-vue to v6 (#1431)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
|
[@vitejs/plugin-vue](https://redirect.github.com/vitejs/vite-plugin-vue/tree/main/packages/plugin-vue#readme)
([source](https://redirect.github.com/vitejs/vite-plugin-vue/tree/HEAD/packages/plugin-vue))
| [`5.2.4` ->
`6.0.0`](https://renovatebot.com/diffs/npm/@vitejs%2fplugin-vue/5.2.4/6.0.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@vitejs%2fplugin-vue/6.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vitejs%2fplugin-vue/5.2.4/6.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>vitejs/vite-plugin-vue (@&#8203;vitejs/plugin-vue)</summary>

###
[`v6.0.0`](https://redirect.github.com/vitejs/vite-plugin-vue/blob/HEAD/packages/plugin-vue/CHANGELOG.md#600-2025-06-24)

##### Bug Fixes

- **deps:** update all non-major dependencies
([#&#8203;590](https://redirect.github.com/vitejs/vite-plugin-vue/issues/590))
([43426c8](43426c8476))
- **deps:** update all non-major dependencies
([#&#8203;600](https://redirect.github.com/vitejs/vite-plugin-vue/issues/600))
([a4c32a8](a4c32a84f3))
- **deps:** update all non-major dependencies
([#&#8203;605](https://redirect.github.com/vitejs/vite-plugin-vue/issues/605))
([67534e5](67534e5d8c))
- **deps:** update all non-major dependencies
([#&#8203;609](https://redirect.github.com/vitejs/vite-plugin-vue/issues/609))
([98c52eb](98c52eb594))

##### Miscellaneous Chores

- add `description` and `keywords` field to package.json
([#&#8203;604](https://redirect.github.com/vitejs/vite-plugin-vue/issues/604))
([67ab76b](67ab76b485))
- **deps:** update dependency rollup to ^4.41.1
([#&#8203;591](https://redirect.github.com/vitejs/vite-plugin-vue/issues/591))
([256ac31](256ac314e6))
- **deps:** update dependency rollup to ^4.43.0
([#&#8203;601](https://redirect.github.com/vitejs/vite-plugin-vue/issues/601))
([a495edf](a495edf617))
- remove Vite 7 beta from supported range
([#&#8203;598](https://redirect.github.com/vitejs/vite-plugin-vue/issues/598))
([c7ddd62](c7ddd625a7))

##### Code Refactoring

- always use `crypto.hash`
([#&#8203;606](https://redirect.github.com/vitejs/vite-plugin-vue/issues/606))
([5de85f6](5de85f6a15))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/unraid/api).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDEuMjMuMiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-11 15:29:14 -04:00
renovate[bot]
e73fc356cb chore(deps): update dependency wrangler to v4 (#1508)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
| [wrangler](https://redirect.github.com/cloudflare/workers-sdk)
([source](https://redirect.github.com/cloudflare/workers-sdk/tree/HEAD/packages/wrangler))
| [`^3.114.10` ->
`^4.0.0`](https://renovatebot.com/diffs/npm/wrangler/3.114.10/4.24.3) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/wrangler/4.24.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/wrangler/3.114.10/4.24.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>cloudflare/workers-sdk (wrangler)</summary>

###
[`v4.24.3`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/wrangler/CHANGELOG.md#4243)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/wrangler@4.24.2...wrangler@4.24.3)

##### Patch Changes

-
[#&#8203;9923](https://redirect.github.com/cloudflare/workers-sdk/pull/9923)
[`c01c4ee`](c01c4ee6af)
Thanks [@&#8203;gpanders](https://redirect.github.com/gpanders)! - Fix
image name resolution when modifying a container application

-
[#&#8203;9833](https://redirect.github.com/cloudflare/workers-sdk/pull/9833)
[`3743896`](3743896120)
Thanks
[@&#8203;dario-piotrowicz](https://redirect.github.com/dario-piotrowicz)!
- fix: ensure that container builds don't disrupt dev hotkey handling

currently container builds run during local development (via `wrangler
dev` or `startWorker`) prevent the standard hotkeys not to be recognized
(most noticeably `ctrl+c`, preventing developers from existing the
process), the changes here ensure that hotkeys are instead correctly
handled as expected

- Updated dependencies \[]:
  - miniflare@4.20250709.0

###
[`v4.24.2`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/wrangler/CHANGELOG.md#4242)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/wrangler@4.24.1...wrangler@4.24.2)

##### Patch Changes

-
[#&#8203;9917](https://redirect.github.com/cloudflare/workers-sdk/pull/9917)
[`80cc834`](80cc83403e)
Thanks [@&#8203;edmundhung](https://redirect.github.com/edmundhung)! -
fix: assets only versions upload should include tag and message

###
[`v4.24.1`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/wrangler/CHANGELOG.md#4241)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/wrangler@4.24.0...wrangler@4.24.1)

##### Patch Changes

-
[#&#8203;9765](https://redirect.github.com/cloudflare/workers-sdk/pull/9765)
[`05adc61`](05adc615c9)
Thanks
[@&#8203;hasip-timurtas](https://redirect.github.com/hasip-timurtas)! -
Build container images without the user's account ID. This allows
containers to be built and verified in dry run mode (where we do not
necessarily have the user's account info).

When we push the image to the managed registry, we first re-tag the
image to include the user's account ID so that the image has the full
resolved image name.

- Updated dependencies
\[[`bb09e50`](bb09e50d8e),
[`25dbe54`](25dbe5480d),
[`3bdec6b`](3bdec6b768)]:
  - miniflare@4.20250709.0

###
[`v4.24.0`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/wrangler/CHANGELOG.md#4240)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/wrangler@4.23.0...wrangler@4.24.0)

##### Minor Changes

-
[#&#8203;9796](https://redirect.github.com/cloudflare/workers-sdk/pull/9796)
[`ba69586`](ba69586d8f)
Thanks [@&#8203;simonabadoiu](https://redirect.github.com/simonabadoiu)!
- Browser Rendering local mode

-
[#&#8203;9825](https://redirect.github.com/cloudflare/workers-sdk/pull/9825)
[`49c85c5`](49c85c5306)
Thanks [@&#8203;ReppCodes](https://redirect.github.com/ReppCodes)! - Add
support for origin\_connection\_limit to Wrangler

  Configure connection limits to Hyperdrive via command line options:

- `--origin-connection-limit`: The (soft) maximum number of connections
that Hyperdrive may establish to the origin database.

-
[#&#8203;9064](https://redirect.github.com/cloudflare/workers-sdk/pull/9064)
[`a1181bf`](a1181bf804)
Thanks [@&#8203;sdnts](https://redirect.github.com/sdnts)! - Added an
`event-subscriptions` subcommand

##### Patch Changes

-
[#&#8203;9729](https://redirect.github.com/cloudflare/workers-sdk/pull/9729)
[`1b3a2b7`](1b3a2b71b7)
Thanks [@&#8203;404Wolf](https://redirect.github.com/404Wolf)! - Set
docker build context to the Dockerfile directory when
`image_build_context` is not explicitly provided

-
[#&#8203;9845](https://redirect.github.com/cloudflare/workers-sdk/pull/9845)
[`dbfa4ef`](dbfa4ef4d4)
Thanks [@&#8203;jonboulle](https://redirect.github.com/jonboulle)! -
remove extraneous double spaces from Wrangler help output

-
[#&#8203;9811](https://redirect.github.com/cloudflare/workers-sdk/pull/9811)
[`fc29c31`](fc29c31ae0)
Thanks [@&#8203;gpanders](https://redirect.github.com/gpanders)! - Fix
unauthorized errors on "containers images delete".

-
[#&#8203;9813](https://redirect.github.com/cloudflare/workers-sdk/pull/9813)
[`45497ab`](45497ab4a4)
Thanks [@&#8203;gpanders](https://redirect.github.com/gpanders)! -
Support container image names without account ID

-
[#&#8203;9821](https://redirect.github.com/cloudflare/workers-sdk/pull/9821)
[`a447d67`](a447d6722a)
Thanks
[@&#8203;WillTaylorDev](https://redirect.github.com/WillTaylorDev)! -
Preview Aliases: Force alias generation to meet stricter naming
requirements.

For cases where CI is requesting Wrangler to generate the alias based on
the branch name, we want a stricter check around the generated alias
name in order to avoid version upload failures. If a valid alias name
was not able to be generated, we warn and do not provide an alias
(avoiding a version upload failure).

-
[#&#8203;9840](https://redirect.github.com/cloudflare/workers-sdk/pull/9840)
[`7c55f9e`](7c55f9e1ea)
Thanks
[@&#8203;dario-piotrowicz](https://redirect.github.com/dario-piotrowicz)!
- fix: make sure that the experimental `remoteBindings` flag is properly
handled in `getPlatformProxy`

There are two issues related to how the experimental `remoteBindings`
flag is handled in `getPlatformProxy` that are being fixed by this
change:

- the `experimental_remote` configuration flag set on service bindings
is incorrectly always taken into account, even if `remoteBindings` is
set to `false`
- the `experimental_remote` configuration flag of all the other bindings
is never taken into account (effectively preventing the bindings to be
used in remote mode) since the `remoteBindings` flag is not being
properly propagated

-
[#&#8203;9801](https://redirect.github.com/cloudflare/workers-sdk/pull/9801)
[`0bb619a`](0bb619a929)
Thanks [@&#8203;IRCody](https://redirect.github.com/IRCody)! -
Containers: Fix issue where setting an image URI instead of dockerfile
would incorrectly not update the image

-
[#&#8203;9872](https://redirect.github.com/cloudflare/workers-sdk/pull/9872)
[`a727db3`](a727db341a)
Thanks [@&#8203;emily-shen](https://redirect.github.com/emily-shen)! -
fix: resolve Dockerfile path relative to the Wrangler config path

This fixes a bug where Wrangler would not be able to find a Dockerfile
if a Wrangler config path had been specified with the `--config` flag.

-
[#&#8203;9815](https://redirect.github.com/cloudflare/workers-sdk/pull/9815)
[`1358034`](1358034ec2)
Thanks [@&#8203;gpanders](https://redirect.github.com/gpanders)! -
Remove --json flag from containers and cloudchamber commands (except for
"images list")

-
[#&#8203;9734](https://redirect.github.com/cloudflare/workers-sdk/pull/9734)
[`1a58bc3`](1a58bc34d6)
Thanks [@&#8203;penalosa](https://redirect.github.com/penalosa)! - Make
Wrangler warn more loudly if you're missing auth scopes

-
[#&#8203;9748](https://redirect.github.com/cloudflare/workers-sdk/pull/9748)
[`7e3aa1b`](7e3aa1b774)
Thanks [@&#8203;alsuren](https://redirect.github.com/alsuren)! -
Internal-only WRANGLER\_D1\_EXTRA\_LOCATION\_CHOICES environment
variable for enabling D1's testing location hints

- Updated dependencies
\[[`ba69586`](ba69586d8f),
[`1a75f85`](1a75f85ae9),
[`395f36d`](395f36de12),
[`6f344bf`](6f344bfe31)]:
  - miniflare@4.20250705.0

###
[`v4.23.0`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/wrangler/CHANGELOG.md#4230)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/wrangler@4.22.0...wrangler@4.23.0)

##### Minor Changes

-
[#&#8203;9535](https://redirect.github.com/cloudflare/workers-sdk/pull/9535)
[`56dc5c4`](56dc5c4946)
Thanks [@&#8203;penalosa](https://redirect.github.com/penalosa)! - In
2023 we announced [breakpoint debugging
support](https://blog.cloudflare.com/debugging-cloudflare-workers/) for
Workers, which meant that you could easily debug your Worker code in
Wrangler's built-in devtools (accessible via the `[d]` hotkey) as well
as multiple other devtools clients, [including
VSCode](https://developers.cloudflare.com/workers/observability/dev-tools/breakpoints/).
For most developers, breakpoint debugging via VSCode is the most natural
flow, but until now it's required [manually configuring a `launch.json`
file](https://developers.cloudflare.com/workers/observability/dev-tools/breakpoints/#setup-vs-code-to-use-breakpoints),
running `wrangler dev`, and connecting via VSCode's built-in debugger.

Now, using VSCode's built-in [JavaScript Debug
Terminals](https://code.visualstudio.com/docs/nodejs/nodejs-debugging#_javascript-debug-terminal),
there are just two steps: open a JS debug terminal and run `wrangler
dev` (or `vite dev`). VSCode will automatically connect to your running
Worker (even if you're running multiple Workers at once!) and start a
debugging session.

-
[#&#8203;9810](https://redirect.github.com/cloudflare/workers-sdk/pull/9810)
[`8acaf43`](8acaf432ac)
Thanks
[@&#8203;WillTaylorDev](https://redirect.github.com/WillTaylorDev)! -
WC-3626 Pull branch name from WORKERS\_CI\_BRANCH if exists.

##### Patch Changes

-
[#&#8203;9775](https://redirect.github.com/cloudflare/workers-sdk/pull/9775)
[`4309bb3`](4309bb30d2)
Thanks [@&#8203;vicb](https://redirect.github.com/vicb)! - Cap the
number of errors and warnings for bulk KV put to avoid consuming too
much memory

-
[#&#8203;9799](https://redirect.github.com/cloudflare/workers-sdk/pull/9799)
[`d11288a`](d11288aff5)
Thanks [@&#8203;penalosa](https://redirect.github.com/penalosa)! -
Better messaging for account owned tokens in `wrangler whoami`

- Updated dependencies
\[[`56dc5c4`](56dc5c4946)]:
  - miniflare@4.20250617.5

###
[`v4.22.0`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/wrangler/CHANGELOG.md#4220)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/wrangler@4.21.2...wrangler@4.22.0)

##### Minor Changes

-
[#&#8203;7871](https://redirect.github.com/cloudflare/workers-sdk/pull/7871)
[`f2a8d4a`](f2a8d4a91e)
Thanks
[@&#8203;dario-piotrowicz](https://redirect.github.com/dario-piotrowicz)!
- add support for assets bindings to `getPlatformProxy`

this change makes sure that that `getPlatformProxy`, when the input
configuration
file contains an assets field, correctly returns the appropriate asset
binding proxy

  example:

  ```jsonc
  // wrangler.jsonc
  {
  	"name": "my-worker",
  	"assets": {
  		"directory": "./public/",
  		"binding": "ASSETS",
  	},
  }
  ```

  ```js
  import { getPlatformProxy } from "wrangler";

  const { env, dispose } = await getPlatformProxy();

const text = await (await
env.ASSETS.fetch("http://0.0.0.0/file.txt")).text();
  console.log(text); // logs the content of file.txt

  await dispose();
  ```

##### Patch Changes

-
[#&#8203;9717](https://redirect.github.com/cloudflare/workers-sdk/pull/9717)
[`d2f2f72`](d2f2f726a1)
Thanks
[@&#8203;nikitassharma](https://redirect.github.com/nikitassharma)! -
Containers should default to a "dev" instance type when no instance type
is specified in the wrangler config

-
[#&#8203;9620](https://redirect.github.com/cloudflare/workers-sdk/pull/9620)
[`1b967ea`](1b967ea0ef)
Thanks [@&#8203;gpanders](https://redirect.github.com/gpanders)! -
Simplify containers images list output format

-
[#&#8203;9684](https://redirect.github.com/cloudflare/workers-sdk/pull/9684)
[`94a340e`](94a340e121)
Thanks
[@&#8203;WillTaylorDev](https://redirect.github.com/WillTaylorDev)! -
Select only successfully deployed deployments when tailing.

###
[`v4.21.2`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/wrangler/CHANGELOG.md#4212)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/wrangler@4.21.1...wrangler@4.21.2)

##### Patch Changes

-
[#&#8203;9731](https://redirect.github.com/cloudflare/workers-sdk/pull/9731)
[`75b75f3`](75b75f3de7)
Thanks [@&#8203;gabivlj](https://redirect.github.com/gabivlj)! -
containers: Check for container scopes before running a container
command to give a better error

-
[#&#8203;9641](https://redirect.github.com/cloudflare/workers-sdk/pull/9641)
[`fdbc9f6`](fdbc9f6048)
Thanks [@&#8203;IRCody](https://redirect.github.com/IRCody)! - Update
container builds to use a more robust method for detecting if the
currently built image already exists.

-
[#&#8203;9736](https://redirect.github.com/cloudflare/workers-sdk/pull/9736)
[`55c83a7`](55c83a7cf9)
Thanks [@&#8203;gabivlj](https://redirect.github.com/gabivlj)! -
containers: Do not check scopes if not defined

-
[#&#8203;9667](https://redirect.github.com/cloudflare/workers-sdk/pull/9667)
[`406fba5`](406fba5fa2)
Thanks [@&#8203;IRCody](https://redirect.github.com/IRCody)! - Fail
earlier in the deploy process when deploying a container worker if
docker is not detected.

###
[`v4.21.1`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/wrangler/CHANGELOG.md#4211)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/wrangler@4.21.0...wrangler@4.21.1)

##### Patch Changes

-
[#&#8203;9626](https://redirect.github.com/cloudflare/workers-sdk/pull/9626)
[`9c938c2`](9c938c2183)
Thanks [@&#8203;penalosa](https://redirect.github.com/penalosa)! -
Support `wrangler version upload` for Python Workers

-
[#&#8203;9718](https://redirect.github.com/cloudflare/workers-sdk/pull/9718)
[`fb83341`](fb83341bed)
Thanks [@&#8203;mhart](https://redirect.github.com/mhart)! - fix error
message when docker daemon is not running

-
[#&#8203;9689](https://redirect.github.com/cloudflare/workers-sdk/pull/9689)
[`b137a6f`](b137a6f090)
Thanks [@&#8203;emily-shen](https://redirect.github.com/emily-shen)! -
fix: correctly pass container engine config to miniflare

-
[#&#8203;9722](https://redirect.github.com/cloudflare/workers-sdk/pull/9722)
[`29e911a`](29e911abbb)
Thanks [@&#8203;emily-shen](https://redirect.github.com/emily-shen)! -
Update containers config schema.

Deprecates `containers.configuration` in favour of top level fields.
Makes top level `image` required. Deprecates `instances` and
`durable_objects`. Makes `name` optional.

-
[#&#8203;9666](https://redirect.github.com/cloudflare/workers-sdk/pull/9666)
[`f3c5791`](f3c5791e3a)
Thanks [@&#8203;IRCody](https://redirect.github.com/IRCody)! - Add a
reasonable default name for containers that have no defined name.

- Updated dependencies
\[[`b137a6f`](b137a6f090)]:
  - miniflare@4.20250617.4

###
[`v4.21.0`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/wrangler/CHANGELOG.md#4210)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/wrangler@4.20.5...wrangler@4.21.0)

##### Minor Changes

-
[#&#8203;9692](https://redirect.github.com/cloudflare/workers-sdk/pull/9692)
[`273952f`](273952ff89)
Thanks [@&#8203;dom96](https://redirect.github.com/dom96)! - Condenses
Python vendored modules in output table

-
[#&#8203;9654](https://redirect.github.com/cloudflare/workers-sdk/pull/9654)
[`2a5988c`](2a5988c50a)
Thanks [@&#8203;dom96](https://redirect.github.com/dom96)! - Python
Workers now automatically bundle .so files from vendored packages

##### Patch Changes

-
[#&#8203;9695](https://redirect.github.com/cloudflare/workers-sdk/pull/9695)
[`0e64c35`](0e64c3515f)
Thanks [@&#8203;emily-shen](https://redirect.github.com/emily-shen)! -
Move hotkey registration later in dev start up

This should have no functional change, but allows us to conditionally
render hotkeys based on config.

-
[#&#8203;9098](https://redirect.github.com/cloudflare/workers-sdk/pull/9098)
[`ef20754`](ef207546d6)
Thanks [@&#8203;jseba](https://redirect.github.com/jseba)! - Migrate
Workers Containers commands to Containers API Endpoints

The Workers Containers API was built on top of Cloudchamber, but has now
been moved to its own API
  with a reduced scoping and new token.

-
[#&#8203;9712](https://redirect.github.com/cloudflare/workers-sdk/pull/9712)
[`2a4c467`](2a4c467d83)
Thanks [@&#8203;emily-shen](https://redirect.github.com/emily-shen)! -
Make `wrangler container` commands print `open-beta` status

###
[`v4.20.5`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/wrangler/CHANGELOG.md#4205)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/wrangler@4.20.4...wrangler@4.20.5)

##### Patch Changes

-
[#&#8203;9688](https://redirect.github.com/cloudflare/workers-sdk/pull/9688)
[`086e29d`](086e29daf4)
Thanks
[@&#8203;dario-piotrowicz](https://redirect.github.com/dario-piotrowicz)!
- add remote bindings support to `getPlatformProxy`

  Example:

  ```json
  // wrangler.jsonc
  {
  	"name": "get-platform-proxy-test",
  	"services": [
  		{
  			"binding": "MY_WORKER",
  			"service": "my-worker",
  			"experimental_remote": true
  		}
  	]
  }
  ```

  ```js
  // index.mjs
  import { getPlatformProxy } from "wrangler";

  const { env } = await getPlatformProxy({
  	experimental: {
  		remoteBindings: true,
  	},
  });

  // env.MY_WORKER.fetch() fetches from the remote my-worker service
  ```

-
[#&#8203;9558](https://redirect.github.com/cloudflare/workers-sdk/pull/9558)
[`d5edf52`](d5edf52b43)
Thanks
[@&#8203;ichernetsky-cf](https://redirect.github.com/ichernetsky-cf)! -
`wrangler containers apply` uses `observability` configuration.

-
[#&#8203;9678](https://redirect.github.com/cloudflare/workers-sdk/pull/9678)
[`24b2c66`](24b2c666cf)
Thanks
[@&#8203;dario-piotrowicz](https://redirect.github.com/dario-piotrowicz)!
- remove warnings during config validations on `experimental_remote`
fields

wrangler commands, run without the `--x-remote-bindings` flag, parsing
config files containing `experimental_remote` fields currently show
warnings stating that the field is not recognized. This is usually more
cumbersome than helpful so here we're loosening up this validation and
making wrangler always recognize the field even when no
`--x-remote-bindings` flag is provided

-
[#&#8203;9633](https://redirect.github.com/cloudflare/workers-sdk/pull/9633)
[`3f478af`](3f478af7f1)
Thanks
[@&#8203;nikitassharma](https://redirect.github.com/nikitassharma)! -
Add support for setting an instance type for containers in wrangler.
This allows users to configure memory, disk, and vCPU by setting
instance type when interacting with containers.

-
[#&#8203;9596](https://redirect.github.com/cloudflare/workers-sdk/pull/9596)
[`5162c51`](5162c51946)
Thanks
[@&#8203;CarmenPopoviciu](https://redirect.github.com/CarmenPopoviciu)!
- add ability to pull images for containers local dev

- Updated dependencies
\[[`bfb791e`](bfb791e708),
[`5162c51`](5162c51946)]:
  - miniflare@4.20250617.3

###
[`v4.20.4`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/wrangler/CHANGELOG.md#4204)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/wrangler@4.20.3...wrangler@4.20.4)

##### Patch Changes

-
[#&#8203;9673](https://redirect.github.com/cloudflare/workers-sdk/pull/9673)
[`ffa742f`](ffa742f32f)
Thanks
[@&#8203;dario-piotrowicz](https://redirect.github.com/dario-piotrowicz)!
- fix: ensure that wrangler deploy and version upload don't override the
remote-bindings flag

-
[#&#8203;9653](https://redirect.github.com/cloudflare/workers-sdk/pull/9653)
[`8a60fe7`](8a60fe76ec)
Thanks [@&#8203;penalosa](https://redirect.github.com/penalosa)! -
Rename `WRANGLER_CONTAINERS_DOCKER_PATH` to `WRANGLER_DOCKER_BIN`

-
[#&#8203;9664](https://redirect.github.com/cloudflare/workers-sdk/pull/9664)
[`c489a44`](c489a44847)
Thanks [@&#8203;IRCody](https://redirect.github.com/IRCody)! - Remove
cloudchamber/container apply confirmation dialog when run
non-interactively.

-
[#&#8203;9653](https://redirect.github.com/cloudflare/workers-sdk/pull/9653)
[`8a60fe7`](8a60fe76ec)
Thanks [@&#8203;penalosa](https://redirect.github.com/penalosa)! - Add a
warning banner to `wrangler cloudchamber` and `wrangler containers`
commands

-
[#&#8203;9605](https://redirect.github.com/cloudflare/workers-sdk/pull/9605)
[`17d23d8`](17d23d8e5f)
Thanks [@&#8203;emily-shen](https://redirect.github.com/emily-shen)! -
Add rebuild hotkey for containers local dev, and clean up containers at
the end of a dev session.

- Updated dependencies
\[[`17d23d8`](17d23d8e5f)]:
  - miniflare@4.20250617.2

###
[`v4.20.3`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/wrangler/CHANGELOG.md#4203)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/wrangler@4.20.2...wrangler@4.20.3)

##### Patch Changes

-
[#&#8203;9621](https://redirect.github.com/cloudflare/workers-sdk/pull/9621)
[`08be3ed`](08be3ed057)
Thanks [@&#8203;gabivlj](https://redirect.github.com/gabivlj)! -
wrangler containers: 'default' scheduling policy should be the default

-
[#&#8203;9586](https://redirect.github.com/cloudflare/workers-sdk/pull/9586)
[`d1d34fe`](d1d34fedd1)
Thanks [@&#8203;penalosa](https://redirect.github.com/penalosa)! -
Remove the Mixed Mode naming in favour of "remote bindings"/"remote
proxy"

- Updated dependencies
\[[`d1d34fe`](d1d34fedd1)]:
  - miniflare@4.20250617.1

###
[`v4.20.2`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/wrangler/CHANGELOG.md#4202)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/wrangler@4.20.1...wrangler@4.20.2)

##### Patch Changes

-
[#&#8203;9565](https://redirect.github.com/cloudflare/workers-sdk/pull/9565)
[`b1c9139`](b1c9139524)
Thanks [@&#8203;IRCody](https://redirect.github.com/IRCody)! - Ensure
that a container applications image configuration is not updated if
there were not changes to the image.

-
[#&#8203;9628](https://redirect.github.com/cloudflare/workers-sdk/pull/9628)
[`92f12f4`](92f12f442d)
Thanks [@&#8203;gpanders](https://redirect.github.com/gpanders)! -
Remove "Cloudchamber" from user facing error messages

-
[#&#8203;9576](https://redirect.github.com/cloudflare/workers-sdk/pull/9576)
[`2671e77`](2671e77843)
Thanks [@&#8203;vicb](https://redirect.github.com/vicb)! - Add core
local dev functionality for containers.
Adds a new WRANGLER\_DOCKER\_HOST env var to customise what socket to
connect to.

- Updated dependencies
\[[`828b7df`](828b7dffad),
[`2671e77`](2671e77843)]:
  - miniflare@4.20250617.0

###
[`v4.20.1`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/wrangler/CHANGELOG.md#4201)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/wrangler@4.20.0...wrangler@4.20.1)

##### Patch Changes

-
[#&#8203;9536](https://redirect.github.com/cloudflare/workers-sdk/pull/9536)
[`3b61c41`](3b61c41f2c)
Thanks
[@&#8203;dario-piotrowicz](https://redirect.github.com/dario-piotrowicz)!
- expose `Unstable_Binding` type

-
[#&#8203;9564](https://redirect.github.com/cloudflare/workers-sdk/pull/9564)
[`1d3293f`](1d3293f0cb)
Thanks [@&#8203;skepticfx](https://redirect.github.com/skepticfx)! -
Switch container registry to `registry.cloudflare.com` from
`registry.cloudchamber.cfdata.org`.
  Also adds the env var `CLOUDFLARE_CONTAINER_REGISTRY` to override this

-
[#&#8203;9520](https://redirect.github.com/cloudflare/workers-sdk/pull/9520)
[`04f9164`](04f9164bbc)
Thanks [@&#8203;vicb](https://redirect.github.com/vicb)! - fix the
default value for keep\_names (`true`)

-
[#&#8203;9506](https://redirect.github.com/cloudflare/workers-sdk/pull/9506)
[`36113c2`](36113c29c8)
Thanks [@&#8203;penalosa](https://redirect.github.com/penalosa)! - Strip
the `CF-Connecting-IP` header from outgoing fetches

-
[#&#8203;9592](https://redirect.github.com/cloudflare/workers-sdk/pull/9592)
[`49f5ac7`](49f5ac7ef2)
Thanks
[@&#8203;petebacondarwin](https://redirect.github.com/petebacondarwin)!
- Point to the right location for docs on telemetry

-
[#&#8203;9593](https://redirect.github.com/cloudflare/workers-sdk/pull/9593)
[`cf33417`](cf33417320)
Thanks [@&#8203;vicb](https://redirect.github.com/vicb)! - drop unused
`WRANGLER_UNENV_RESOLVE_PATHS` env var

-
[#&#8203;9566](https://redirect.github.com/cloudflare/workers-sdk/pull/9566)
[`521eeb9`](521eeb9d7d)
Thanks [@&#8203;vicb](https://redirect.github.com/vicb)! - Bump
`@cloudflare/unenv-preset` to 2.3.3

-
[#&#8203;9344](https://redirect.github.com/cloudflare/workers-sdk/pull/9344)
[`02e2c1e`](02e2c1e4de)
Thanks
[@&#8203;dario-piotrowicz](https://redirect.github.com/dario-piotrowicz)!
- add warning about env not specified to potentially risky wrangler
commands

add a warning suggesting users to specify their target environment (via
`-e` or `--env`)
when their wrangler config file contains some environments and they are
calling one
  of the following commands:

  - wrangler deploy
  - wrangler versions upload
  - wrangler versions deploy
  - wrangler versions secret bulk
  - wrangler versions secret put
  - wrangler versions secret delete
  - wrangler secret bulk
  - wrangler secret put
  - wrangler secret delete
  - wrangler triggers deploy

this is a measure we're putting in place to try to prevent developers
from accidentally applying
  changes to an incorrect (potentially even production) environment

-
[#&#8203;9344](https://redirect.github.com/cloudflare/workers-sdk/pull/9344)
[`02e2c1e`](02e2c1e4de)
Thanks
[@&#8203;dario-piotrowicz](https://redirect.github.com/dario-piotrowicz)!
- allow passing an empty string to the `-e|--env` flag to target the
top-level environment

-
[#&#8203;9536](https://redirect.github.com/cloudflare/workers-sdk/pull/9536)
[`3b61c41`](3b61c41f2c)
Thanks
[@&#8203;dario-piotrowicz](https://redirect.github.com/dario-piotrowicz)!
- performance improvement: restart a mixed mode session only if the
worker's remote bindings have changed

-
[#&#8203;9550](https://redirect.github.com/cloudflare/workers-sdk/pull/9550)
[`c117904`](c11790486f)
Thanks
[@&#8203;dario-piotrowicz](https://redirect.github.com/dario-piotrowicz)!
- allow `startWorker` to accept `false` as an `inspector` option (to
disable the inspector server)

-
[#&#8203;9473](https://redirect.github.com/cloudflare/workers-sdk/pull/9473)
[`fae8c02`](fae8c02bcf)
Thanks
[@&#8203;dario-piotrowicz](https://redirect.github.com/dario-piotrowicz)!
- expose new `experimental_maybeStartOrUpdateMixedModeSession` utility

- Updated dependencies
\[[`bd528d5`](bd528d5d53),
[`2177fb4`](2177fb44f4),
[`36113c2`](36113c29c8),
[`e16fcc7`](e16fcc747a)]:
  - miniflare@4.20250612.0

###
[`v4.20.0`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/wrangler/CHANGELOG.md#4200)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/wrangler@4.19.2...wrangler@4.20.0)

##### Minor Changes

-
[#&#8203;9509](https://redirect.github.com/cloudflare/workers-sdk/pull/9509)
[`0b2ba45`](0b2ba4590c)
Thanks [@&#8203;emily-shen](https://redirect.github.com/emily-shen)! -
feat: add static routing options via 'run\_worker\_first' to Wrangler

Implements the proposal noted here
[https://github.com/cloudflare/workers-sdk/discussions/9143](https://redirect.github.com/cloudflare/workers-sdk/discussions/9143).

This is now usable in `wrangler dev` and in production - just specify
the routes that should hit the worker first with `run_worker_first` in
your Wrangler config. You can also omit certain paths with `!` negative
rules.

##### Patch Changes

-
[#&#8203;9507](https://redirect.github.com/cloudflare/workers-sdk/pull/9507)
[`1914b87`](1914b87e25)
Thanks
[@&#8203;dario-piotrowicz](https://redirect.github.com/dario-piotrowicz)!
- slightly improve wrangler dev bindings loggings

  improve the bindings loggings by:

- removing the unnecessary (and potentially incorrect) `[connected]`
suffix for remote bindings
- making sure that the modes presented in the bindings logs are
correctly aligned

-
[#&#8203;9475](https://redirect.github.com/cloudflare/workers-sdk/pull/9475)
[`931f467`](931f467e39)
Thanks [@&#8203;edmundhung](https://redirect.github.com/edmundhung)! -
add hello world binding that serves as as an explanatory example.

-
[#&#8203;9443](https://redirect.github.com/cloudflare/workers-sdk/pull/9443)
[`95eb47d`](95eb47d2c6)
Thanks
[@&#8203;dario-piotrowicz](https://redirect.github.com/dario-piotrowicz)!
- add workerName option to startMixedModeSession API

-
[#&#8203;9541](https://redirect.github.com/cloudflare/workers-sdk/pull/9541)
[`80b8bd9`](80b8bd93e6)
Thanks
[@&#8203;dario-piotrowicz](https://redirect.github.com/dario-piotrowicz)!
- make workers created with `startWorker` await the `ready` promise on
`dispose`

-
[#&#8203;9443](https://redirect.github.com/cloudflare/workers-sdk/pull/9443)
[`95eb47d`](95eb47d2c6)
Thanks
[@&#8203;dario-piotrowicz](https://redirect.github.com/dario-piotrowicz)!
- add mixed-mode support for mtls bindings

-
[#&#8203;9515](https://redirect.github.com/cloudflare/workers-sdk/pull/9515)
[`9e4cd16`](9e4cd16ce1)
Thanks
[@&#8203;dario-piotrowicz](https://redirect.github.com/dario-piotrowicz)!
- make sure that remote binding errors are surfaced when using mixed
(hybrid) mode

-
[#&#8203;9516](https://redirect.github.com/cloudflare/workers-sdk/pull/9516)
[`92305af`](92305af0a7)
Thanks [@&#8203;IRCody](https://redirect.github.com/IRCody)! - Reorder
deploy output when deploying a container worker so the worker url is
printed last and the worker triggers aren't deployed until the container
has been built and deployed successfully.

- Updated dependencies
\[[`931f467`](931f467e39),
[`95eb47d`](95eb47d2c6),
[`0b2ba45`](0b2ba4590c)]:
  - miniflare@4.20250604.1
-
[@&#8203;cloudflare/unenv-preset](https://redirect.github.com/cloudflare/unenv-preset)@&#8203;2.3.3

###
[`v4.19.2`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/wrangler/CHANGELOG.md#4192)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/wrangler@4.19.1...wrangler@4.19.2)

##### Patch Changes

-
[#&#8203;9461](https://redirect.github.com/cloudflare/workers-sdk/pull/9461)
[`66edd2f`](66edd2f3bd)
Thanks [@&#8203;skepticfx](https://redirect.github.com/skepticfx)! -
Enforce disk limits on container builds

-
[#&#8203;9481](https://redirect.github.com/cloudflare/workers-sdk/pull/9481)
[`d1a1787`](d1a1787b27)
Thanks
[@&#8203;WillTaylorDev](https://redirect.github.com/WillTaylorDev)! -
Force autogenerated aliases to be fully lowercased.

-
[#&#8203;9480](https://redirect.github.com/cloudflare/workers-sdk/pull/9480)
[`1f84092`](1f84092851)
Thanks
[@&#8203;dario-piotrowicz](https://redirect.github.com/dario-piotrowicz)!
- add `experimentalMixedMode` dev option to `unstable_startWorker`

add an new `experimentalMixedMode` dev option to `unstable_startWorker`
  that allows developers to programmatically start a new mixed mode
  session using startWorker.

  Example usage:

  ```js
  // index.mjs
  import { unstable_startWorker } from "wrangler";

  await unstable_startWorker({
  	dev: {
  		experimentalMixedMode: true,
  	},
  });
  ```

  ```json
  // wrangler.jsonc
  {
  	"$schema": "node_modules/wrangler/config-schema.json",
  	"name": "programmatic-start-worker-example",
  	"main": "src/index.ts",
  	"compatibility_date": "2025-06-01",
  	"services": [
{ "binding": "REMOTE_WORKER", "service": "remote-worker", "remote": true
}
  	]
  }
  ```

- Updated dependencies
\[[`4ab5a40`](4ab5a4027d),
[`485cd08`](485cd08679),
[`e3b3ef5`](e3b3ef51cf),
[`3261957`](3261957aba)]:
  - miniflare@4.20250604.0
-
[@&#8203;cloudflare/unenv-preset](https://redirect.github.com/cloudflare/unenv-preset)@&#8203;2.3.3

###
[`v4.19.1`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/wrangler/CHANGELOG.md#4191)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/wrangler@4.19.0...wrangler@4.19.1)

##### Patch Changes

-
[#&#8203;9456](https://redirect.github.com/cloudflare/workers-sdk/pull/9456)
[`db2cdc6`](db2cdc6b1e)
Thanks
[@&#8203;WillTaylorDev](https://redirect.github.com/WillTaylorDev)! -
Fix bug causing preview alias to always be generated.

###
[`v4.19.0`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/wrangler/CHANGELOG.md#4190)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/wrangler@4.18.0...wrangler@4.19.0)

##### Minor Changes

-
[#&#8203;9401](https://redirect.github.com/cloudflare/workers-sdk/pull/9401)
[`03b8c1c`](03b8c1ca53)
Thanks
[@&#8203;WillTaylorDev](https://redirect.github.com/WillTaylorDev)! -
Provide ability for Wrangler to upload preview aliases during version
upload.

##### Patch Changes

-
[#&#8203;9390](https://redirect.github.com/cloudflare/workers-sdk/pull/9390)
[`80e75f4`](80e75f4a67)
Thanks [@&#8203;penalosa](https://redirect.github.com/penalosa)! -
Support additional Mixed Mode resources in Wrangler:

  - AI
  - Browser
  - Images
  - Vectorize
  - Dispatch Namespaces

-
[#&#8203;9395](https://redirect.github.com/cloudflare/workers-sdk/pull/9395)
[`b3be057`](b3be057344)
Thanks [@&#8203;Maximo-Guk](https://redirect.github.com/Maximo-Guk)! -
Add WRANGLER\_CI\_OVERRIDE\_NETWORK\_MODE\_HOST for Workers CI

-
[#&#8203;9410](https://redirect.github.com/cloudflare/workers-sdk/pull/9410)
[`87f3843`](87f38432ee)
Thanks
[@&#8203;dario-piotrowicz](https://redirect.github.com/dario-piotrowicz)!
- enable consumers of `unstable_readConfig` to silence `remote` warnings

- Updated dependencies
\[[`8c7ce77`](8c7ce7728c),
[`80e75f4`](80e75f4a67),
[`80e75f4`](80e75f4a67),
[`fac2f9d`](fac2f9dfa6),
[`92719a5`](92719a535b)]:
  - miniflare@4.20250525.1

###
[`v4.18.0`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/wrangler/CHANGELOG.md#4180)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/wrangler@4.17.0...wrangler@4.18.0)

##### Minor Changes

-
[#&#8203;9393](https://redirect.github.com/cloudflare/workers-sdk/pull/9393)
[`34b6174`](34b61746f2)
Thanks [@&#8203;jamesopstad](https://redirect.github.com/jamesopstad)! -
Hard fail on Node.js < 20. Wrangler no longer supports Node.js 18.x as
it reached end-of-life on 2025-04-30. See
https://github.com/nodejs/release?tab=readme-ov-file#end-of-life-releases.

##### Patch Changes

-
[#&#8203;9308](https://redirect.github.com/cloudflare/workers-sdk/pull/9308)
[`d3a6eb3`](d3a6eb30e5)
Thanks
[@&#8203;dario-piotrowicz](https://redirect.github.com/dario-piotrowicz)!
- expose new utilities and types to aid consumers of the programmatic
mixed-mode API

  Specifically the exports have been added:

- `Experimental_MixedModeSession`: type representing a mixed-mode
session
- `Experimental_ConfigBindingsOptions`: type representing
config-bindings
- `experimental_pickRemoteBindings`: utility for picking only the remote
bindings from a record of start-worker bindings.
- `unstable_convertConfigBindingsToStartWorkerBindings`: utility for
converting config-bindings into start-worker bindings (that can be
passed to `startMixedModeSession`)

-
[#&#8203;9347](https://redirect.github.com/cloudflare/workers-sdk/pull/9347)
[`b8f058c`](b8f058c81e)
Thanks [@&#8203;penalosa](https://redirect.github.com/penalosa)! -
Improve binding display on narrower terminals

- Updated dependencies
\[[`d9d937a`](d9d937ab6f),
[`e39a45f`](e39a45ffa0),
[`fdae3f7`](fdae3f7665)]:
  - miniflare@4.20250525.0

###
[`v4.17.0`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/wrangler/CHANGELOG.md#4170)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/wrangler@4.16.1...wrangler@4.17.0)

##### Minor Changes

-
[#&#8203;9321](https://redirect.github.com/cloudflare/workers-sdk/pull/9321)
[`6c03bde`](6c03bde33f)
Thanks
[@&#8203;petebacondarwin](https://redirect.github.com/petebacondarwin)!
- Add support for FedRAMP High compliance region

Now it is possible to target Wrangler at the FedRAMP High compliance
region.
  There are two ways to signal to Wrangler to run in this mode:

- set `"compliance_region": "fedramp_high"` in a Wrangler configuration
- set `CLOUDFLARE_COMPLIANCE_REGION=fedramp_high` environment variable
when running Wrangler

If both are provided and the values do not match then Wrangler will exit
with an error.

  When in this mode OAuth authentication is not supported.
It is necessary to authenticate using a Cloudflare API Token acquired
from the Cloudflare FedRAMP High dashboard.

  Most bindings and commands are supported in this mode.

- Unsupported commands may result in API requests that are not supported
- possibly 422 Unprocessable Entity responses.
- Unsupported bindings may work in local dev, as there is no local
validation, but will fail at Worker deployment time.

  Resolves DEVX-1921.

-
[#&#8203;9330](https://redirect.github.com/cloudflare/workers-sdk/pull/9330)
[`34c71ce`](34c71ce920)
Thanks [@&#8203;edmundhung](https://redirect.github.com/edmundhung)! -
Updated internal configuration to use Miniflare’s new
`defaultPersistRoot` instead of per-plugin `persist` flags

-
[#&#8203;8973](https://redirect.github.com/cloudflare/workers-sdk/pull/8973)
[`cc7fae4`](cc7fae4cb9)
Thanks
[@&#8203;Caio-Nogueira](https://redirect.github.com/Caio-Nogueira)! -
Show latest instance by default on `workflows instances describe`
command

##### Patch Changes

-
[#&#8203;9335](https://redirect.github.com/cloudflare/workers-sdk/pull/9335)
[`6479fc5`](6479fc5228)
Thanks [@&#8203;penalosa](https://redirect.github.com/penalosa)! -
Redesign `wrangler dev` to more clearly present information and have a
bit of a glow up 
![Screenshot 2025-05-22 at 01 11
43](https://redirect.github.com/user-attachments/assets/26cc6209-37a1-4ecb-8e91-daac2f79a095)

-
[#&#8203;9329](https://redirect.github.com/cloudflare/workers-sdk/pull/9329)
[`410d985`](410d985250)
Thanks [@&#8203;penalosa](https://redirect.github.com/penalosa)! - Hide
logs in the `startMixedModeSession()` API

-
[#&#8203;9325](https://redirect.github.com/cloudflare/workers-sdk/pull/9325)
[`c2678d1`](c2678d1681)
Thanks [@&#8203;edmundhung](https://redirect.github.com/edmundhung)! -
refactor: fallbacks to local image binding from miniflare when local
mode is enabled

- Updated dependencies
\[[`34c71ce`](34c71ce920),
[`f7c82a4`](f7c82a4a9f),
[`7ddd865`](7ddd865fa6),
[`6479fc5`](6479fc5228),
[`e5ae13a`](e5ae13adeb)]:
  - miniflare@4.20250523.0

###
[`v4.16.1`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/wrangler/CHANGELOG.md#4161)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/wrangler@4.16.0...wrangler@4.16.1)

##### Patch Changes

-
[#&#8203;9268](https://redirect.github.com/cloudflare/workers-sdk/pull/9268)
[`7344344`](734434418f)
Thanks [@&#8203;gabivlj](https://redirect.github.com/gabivlj)! -
`wrangler containers delete` handles API errors correctly

###
[`v4.16.0`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/wrangler/CHANGELOG.md#4160)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/wrangler@4.15.2...wrangler@4.16.0)

##### Minor Changes

-
[#&#8203;9288](https://redirect.github.com/cloudflare/workers-sdk/pull/9288)
[`3b8f7f1`](3b8f7f18be)
Thanks
[@&#8203;petebacondarwin](https://redirect.github.com/petebacondarwin)!
- allow --name and --env args on wrangler deploy

Previously it was not possible to provide a Worker name as a command
line argument at the same time as setting the Wrangler environment.
Now specifying `--name` is supported and will override any names set in
the Wrangler config:

  **wrangler.json**

  ```json
  {
  	"name": "config-worker"
  	"env": {
  		"staging": { "name": "config-worker-env" }
  	}
  }
  ```

| Command | Previous (Worker name) | Proposed (Worker name) | Comment |
| ------------------------------------------------ |
---------------------- | ---------------------- |
------------------------------------- |
| wrangler deploy --name=args-worker | "args-worker" | "args-worker" |
CLI arg used |
| wrangler deploy --name=args-worker --env=staging | *Error* |
"args-worker" | CLI arg used |
| wrangler deploy --name=args-worker --env=prod | *Error* |
"args-worker" | CLI arg used |
| wrangler deploy | "config-worker" | "config-worker" | Top-level config
used |
| wrangler deploy --env=staging | "config-worker-env" |
"config-worker-env" | Named env config used |
| wrangler deploy --env=prod | "config-worker-prod" |
"config-worker-prod" | CLI arg and top-level config combined |

-
[#&#8203;9265](https://redirect.github.com/cloudflare/workers-sdk/pull/9265)
[`16de0d5`](16de0d5227)
Thanks [@&#8203;edmundhung](https://redirect.github.com/edmundhung)! -
docs: add documentation links to individual config properties in the
JSON schema of the Wrangler config file

##### Patch Changes

-
[#&#8203;9234](https://redirect.github.com/cloudflare/workers-sdk/pull/9234)
[`2fe6219`](2fe62198d7)
Thanks [@&#8203;emily-shen](https://redirect.github.com/emily-shen)! -
fix: add no-op `props` to `ctx` in `getPlatformProxy` to fix type
mismatch

-
[#&#8203;9269](https://redirect.github.com/cloudflare/workers-sdk/pull/9269)
[`66d975e`](66d975e905)
Thanks
[@&#8203;dario-piotrowicz](https://redirect.github.com/dario-piotrowicz)!
- Wire up mixed-mode remote bindings for multi-worker `wrangler dev`

Under the `--x-mixed-mode` flag, make sure that bindings configurations
with `remote: true` actually generate bindings to remote resources
during a multi-worker `wrangler dev` session, currently the bindings
included in this are: services, kv\_namespaces, r2\_buckets,
d1\_databases, queues and workflows.

Also include the ai binding since the bindings is already remote by
default anyways.

-
[#&#8203;9151](https://redirect.github.com/cloudflare/workers-sdk/pull/9151)
[`5ab035d`](5ab035d8a1)
Thanks [@&#8203;gabivlj](https://redirect.github.com/gabivlj)! -
wrangler containers can be configured with the kind of application
rollout on `apply`

-
[#&#8203;9231](https://redirect.github.com/cloudflare/workers-sdk/pull/9231)
[`02d40ed`](02d40ed3bb)
Thanks
[@&#8203;dario-piotrowicz](https://redirect.github.com/dario-piotrowicz)!
- Wire up mixed-mode remote bindings for (single-worker) `wrangler dev`

Under the `--x-mixed-mode` flag, make sure that bindings configurations
with `remote: true` actually generate bindings to remote resources
during a single-worker `wrangler dev` session, currently the bindings
included in this are: services, kv\_namespaces, r2\_buckets,
d1\_databases, queues and workflows.

Also include the ai binding since the bindings is already remote by
default anyways.

-
[#&#8203;9221](https://redirect.github.com/cloudflare/workers-sdk/pull/9221)
[`2ef31a9`](2ef31a9459)
Thanks [@&#8203;vicb](https://redirect.github.com/vicb)! - bump
`@cloudflare/unenv-preset`

-
[#&#8203;9277](https://redirect.github.com/cloudflare/workers-sdk/pull/9277)
[`db5ea8f`](db5ea8f1f6)
Thanks [@&#8203;penalosa](https://redirect.github.com/penalosa)! -
Support Mixed Mode for more binding types

-
[#&#8203;9266](https://redirect.github.com/cloudflare/workers-sdk/pull/9266)
[`f2a16f1`](f2a16f1126)
Thanks
[@&#8203;petebacondarwin](https://redirect.github.com/petebacondarwin)!
- fix: setting triggers.crons:\[] in Wrangler config should delete
deployed cron schedules

-
[#&#8203;9245](https://redirect.github.com/cloudflare/workers-sdk/pull/9245)
[`b87b472`](b87b472a1a)
Thanks [@&#8203;penalosa](https://redirect.github.com/penalosa)! -
Support Mixed Mode Dispatch Namespaces

- Updated dependencies
\[[`db5ea8f`](db5ea8f1f6),
[`b87b472`](b87b472a1a)]:
  - miniflare@4.20250508.3

###
[`v4.15.2`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/wrangler/CHANGELOG.md#4152)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/wrangler@4.15.1...wrangler@4.15.2)

##### Patch Changes

-
[#&#8203;9257](https://redirect.github.com/cloudflare/workers-sdk/pull/9257)
[`33daa09`](33daa0961f)
Thanks [@&#8203;penalosa](https://redirect.github.com/penalosa)! - Relax
R2 bucket validation for `pages dev` commands

-
[#&#8203;9256](https://redirect.github.com/cloudflare/workers-sdk/pull/9256)
[`3b384e2`](3b384e28c7)
Thanks [@&#8203;penalosa](https://redirect.github.com/penalosa)! - Move
the Analytics Engine simulator implementation from JSRPC to a Wrapped
binding. This fixes a regression introduced in
[https://github.com/cloudflare/workers-sdk/pull/8935](https://redirect.github.com/cloudflare/workers-sdk/pull/8935)
that preventing Analytics Engine bindings working in local dev for
Workers with a compatibility date prior to JSRPC being enabled.

- Updated dependencies
\[[`3b384e2`](3b384e28c7)]:
  - miniflare@4.20250508.2
-
[@&#8203;cloudflare/unenv-preset](https://redirect.github.com/cloudflare/unenv-preset)@&#8203;2.3.2

###
[`v4.15.1`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/wrangler/CHANGELOG.md#4151)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/wrangler@4.15.0...wrangler@4.15.1)

##### Patch Changes

-
[#&#8203;9248](https://redirect.github.com/cloudflare/workers-sdk/pull/9248)
[`07f4010`](07f4010e6d)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/unraid/api).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4yMy4yIiwidXBkYXRlZEluVmVyIjoiNDEuMjMuMiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-11 15:27:20 -04:00
renovate[bot]
e1a7a3d22d chore(deps): update dependency node to v22 (#1507)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [node](https://redirect.github.com/actions/node-versions) | uses-with
| major | `20` -> `22` |

---

### Release Notes

<details>
<summary>actions/node-versions (node)</summary>

###
[`v22.17.0`](https://redirect.github.com/actions/node-versions/releases/tag/22.17.0-15866718879):
22.17.0

[Compare
Source](https://redirect.github.com/actions/node-versions/compare/22.16.0-15177438473...22.17.0-15866718879)

Node.js 22.17.0

###
[`v22.16.0`](https://redirect.github.com/actions/node-versions/releases/tag/22.16.0-15177438473):
22.16.0

[Compare
Source](https://redirect.github.com/actions/node-versions/compare/22.15.1-15035854612...22.16.0-15177438473)

Node.js 22.16.0

###
[`v22.15.1`](https://redirect.github.com/actions/node-versions/releases/tag/22.15.1-15035854612):
22.15.1

[Compare
Source](https://redirect.github.com/actions/node-versions/compare/22.15.0-14621731016...22.15.1-15035854612)

Node.js 22.15.1

###
[`v22.15.0`](https://redirect.github.com/actions/node-versions/releases/tag/22.15.0-14621731016):
22.15.0

[Compare
Source](https://redirect.github.com/actions/node-versions/compare/22.14.0-13265982013...22.15.0-14621731016)

Node.js 22.15.0

###
[`v22.14.0`](https://redirect.github.com/actions/node-versions/releases/tag/22.14.0-13265982013):
22.14.0

[Compare
Source](https://redirect.github.com/actions/node-versions/compare/22.13.1-12900459766...22.14.0-13265982013)

Node.js 22.14.0

###
[`v22.13.1`](https://redirect.github.com/actions/node-versions/releases/tag/22.13.1-12900459766):
22.13.1

[Compare
Source](https://redirect.github.com/actions/node-versions/compare/22.13.0-12671059536...22.13.1-12900459766)

Node.js 22.13.1

###
[`v22.13.0`](https://redirect.github.com/actions/node-versions/releases/tag/22.13.0-12671059536):
22.13.0

[Compare
Source](https://redirect.github.com/actions/node-versions/compare/22.12.0-12152383658...22.13.0-12671059536)

Node.js 22.13.0

###
[`v22.12.0`](https://redirect.github.com/actions/node-versions/releases/tag/22.12.0-12152383658):
22.12.0

[Compare
Source](https://redirect.github.com/actions/node-versions/compare/22.11.0-11593095476...22.12.0-12152383658)

Node.js 22.12.0

###
[`v22.11.0`](https://redirect.github.com/actions/node-versions/releases/tag/22.11.0-11593095476):
22.11.0

[Compare
Source](https://redirect.github.com/actions/node-versions/compare/22.10.0-11377615849...22.11.0-11593095476)

Node.js 22.11.0

###
[`v22.10.0`](https://redirect.github.com/actions/node-versions/releases/tag/22.10.0-11377615849):
22.10.0

[Compare
Source](https://redirect.github.com/actions/node-versions/compare/22.9.0-10914884886...22.10.0-11377615849)

Node.js 22.10.0

###
[`v22.9.0`](https://redirect.github.com/actions/node-versions/releases/tag/22.9.0-10914884886):
22.9.0

[Compare
Source](https://redirect.github.com/actions/node-versions/compare/22.8.0-10685632420...22.9.0-10914884886)

Node.js 22.9.0

###
[`v22.8.0`](https://redirect.github.com/actions/node-versions/releases/tag/22.8.0-10685632420):
22.8.0

[Compare
Source](https://redirect.github.com/actions/node-versions/compare/22.7.0-10511334152...22.8.0-10685632420)

Node.js 22.8.0

###
[`v22.7.0`](https://redirect.github.com/actions/node-versions/releases/tag/22.7.0-10511334152):
22.7.0

[Compare
Source](https://redirect.github.com/actions/node-versions/compare/22.6.0-10277432289...22.7.0-10511334152)

Node.js 22.7.0

###
[`v22.6.0`](https://redirect.github.com/actions/node-versions/releases/tag/22.6.0-10277432289):
22.6.0

[Compare
Source](https://redirect.github.com/actions/node-versions/compare/22.5.1-10010673511...22.6.0-10277432289)

Node.js 22.6.0

###
[`v22.5.1`](https://redirect.github.com/actions/node-versions/releases/tag/22.5.1-10010673511):
22.5.1

[Compare
Source](https://redirect.github.com/actions/node-versions/compare/22.5.0-9985144103...22.5.1-10010673511)

Node.js 22.5.1

###
[`v22.5.0`](https://redirect.github.com/actions/node-versions/releases/tag/22.5.0-9985144103):
22.5.0

[Compare
Source](https://redirect.github.com/actions/node-versions/compare/22.4.1-9860948056...22.5.0-9985144103)

Node.js 22.5.0

###
[`v22.4.1`](https://redirect.github.com/actions/node-versions/releases/tag/22.4.1-9860948056):
22.4.1

[Compare
Source](https://redirect.github.com/actions/node-versions/compare/22.4.0-9766506602...22.4.1-9860948056)

Node.js 22.4.1

###
[`v22.4.0`](https://redirect.github.com/actions/node-versions/releases/tag/22.4.0-9766506602):
22.4.0

[Compare
Source](https://redirect.github.com/actions/node-versions/compare/22.3.0-9569309553...22.4.0-9766506602)

Node.js 22.4.0

###
[`v22.3.0`](https://redirect.github.com/actions/node-versions/releases/tag/22.3.0-9569309553):
22.3.0

[Compare
Source](https://redirect.github.com/actions/node-versions/compare/22.2.0-9105861751...22.3.0-9569309553)

Node.js 22.3.0

###
[`v22.2.0`](https://redirect.github.com/actions/node-versions/releases/tag/22.2.0-9105861751):
22.2.0

[Compare
Source](https://redirect.github.com/actions/node-versions/compare/22.1.0-8926142033...22.2.0-9105861751)

Node.js 22.2.0

###
[`v22.1.0`](https://redirect.github.com/actions/node-versions/releases/tag/22.1.0-8926142033):
22.1.0

[Compare
Source](https://redirect.github.com/actions/node-versions/compare/22.0.0-8879734543...22.1.0-8926142033)

Node.js 22.1.0

###
[`v22.0.0`](https://redirect.github.com/actions/node-versions/releases/tag/22.0.0-8879734543):
22.0.0

[Compare
Source](https://redirect.github.com/actions/node-versions/compare/20.19.3-15828158811...22.0.0-8879734543)

Node.js 22.0.0

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/unraid/api).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4yMy4yIiwidXBkYXRlZEluVmVyIjoiNDEuMjMuMiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-11 14:41:02 -04:00
renovate[bot]
53b05ebe5e fix(deps): update all non-major dependencies (#1489)
This PR contains the following updates:

| Package | Change | Age | Confidence | Type | Update |
|---|---|---|---|---|---|
| [@eslint/js](https://eslint.org)
([source](https://redirect.github.com/eslint/eslint/tree/HEAD/packages/js))
| [`9.29.0` ->
`9.30.1`](https://renovatebot.com/diffs/npm/@eslint%2fjs/9.29.0/9.30.1)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@eslint%2fjs/9.30.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@eslint%2fjs/9.29.0/9.30.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
| [@faker-js/faker](https://fakerjs.dev)
([source](https://redirect.github.com/faker-js/faker)) | [`9.8.0` ->
`9.9.0`](https://renovatebot.com/diffs/npm/@faker-js%2ffaker/9.8.0/9.9.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@faker-js%2ffaker/9.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@faker-js%2ffaker/9.8.0/9.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
| [@floating-ui/dom](https://floating-ui.com)
([source](https://redirect.github.com/floating-ui/floating-ui/tree/HEAD/packages/dom))
| [`1.7.1` ->
`1.7.2`](https://renovatebot.com/diffs/npm/@floating-ui%2fdom/1.7.1/1.7.2)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@floating-ui%2fdom/1.7.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@floating-ui%2fdom/1.7.1/1.7.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [@floating-ui/utils](https://floating-ui.com)
([source](https://redirect.github.com/floating-ui/floating-ui/tree/HEAD/packages/utils))
| [`0.2.9` ->
`0.2.10`](https://renovatebot.com/diffs/npm/@floating-ui%2futils/0.2.9/0.2.10)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@floating-ui%2futils/0.2.10?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@floating-ui%2futils/0.2.9/0.2.10?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [@floating-ui/vue](https://floating-ui.com/docs/vue)
([source](https://redirect.github.com/floating-ui/floating-ui/tree/HEAD/packages/vue))
| [`1.1.6` ->
`1.1.7`](https://renovatebot.com/diffs/npm/@floating-ui%2fvue/1.1.6/1.1.7)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@floating-ui%2fvue/1.1.7?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@floating-ui%2fvue/1.1.6/1.1.7?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
|
[@graphql-codegen/client-preset](https://redirect.github.com/dotansimha/graphql-code-generator)
([source](https://redirect.github.com/dotansimha/graphql-code-generator/tree/HEAD/packages/presets/client))
| [`4.8.2` ->
`4.8.3`](https://renovatebot.com/diffs/npm/@graphql-codegen%2fclient-preset/4.8.2/4.8.3)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@graphql-codegen%2fclient-preset/4.8.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@graphql-codegen%2fclient-preset/4.8.2/4.8.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
|
[@graphql-codegen/client-preset](https://redirect.github.com/dotansimha/graphql-code-generator)
([source](https://redirect.github.com/dotansimha/graphql-code-generator/tree/HEAD/packages/presets/client))
| [`4.8.2` ->
`4.8.3`](https://renovatebot.com/diffs/npm/@graphql-codegen%2fclient-preset/4.8.2/4.8.3)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@graphql-codegen%2fclient-preset/4.8.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@graphql-codegen%2fclient-preset/4.8.2/4.8.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
|
[@graphql-codegen/typed-document-node](https://redirect.github.com/dotansimha/graphql-code-generator)
([source](https://redirect.github.com/dotansimha/graphql-code-generator/tree/HEAD/packages/plugins/typescript/typed-document-node))
| [`5.1.1` ->
`5.1.2`](https://renovatebot.com/diffs/npm/@graphql-codegen%2ftyped-document-node/5.1.1/5.1.2)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@graphql-codegen%2ftyped-document-node/5.1.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@graphql-codegen%2ftyped-document-node/5.1.1/5.1.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
|
[@ianvs/prettier-plugin-sort-imports](https://redirect.github.com/ianvs/prettier-plugin-sort-imports)
| [`4.4.2` ->
`4.5.1`](https://renovatebot.com/diffs/npm/@ianvs%2fprettier-plugin-sort-imports/4.4.2/4.5.1)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@ianvs%2fprettier-plugin-sort-imports/4.5.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@ianvs%2fprettier-plugin-sort-imports/4.4.2/4.5.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
| [@nuxt/devtools](https://devtools.nuxt.com)
([source](https://redirect.github.com/nuxt/devtools/tree/HEAD/packages/devtools))
| [`2.5.0` ->
`2.6.2`](https://renovatebot.com/diffs/npm/@nuxt%2fdevtools/2.5.0/2.6.2)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@nuxt%2fdevtools/2.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nuxt%2fdevtools/2.5.0/2.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
| [@nuxt/eslint](https://redirect.github.com/nuxt/eslint)
([source](https://redirect.github.com/nuxt/eslint/tree/HEAD/packages/module))
| [`1.4.1` ->
`1.5.2`](https://renovatebot.com/diffs/npm/@nuxt%2feslint/1.4.1/1.5.2) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/@nuxt%2feslint/1.5.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nuxt%2feslint/1.4.1/1.5.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
| [@nuxt/test-utils](https://redirect.github.com/nuxt/test-utils) |
[`3.19.1` ->
`3.19.2`](https://renovatebot.com/diffs/npm/@nuxt%2ftest-utils/3.19.1/3.19.2)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@nuxt%2ftest-utils/3.19.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nuxt%2ftest-utils/3.19.1/3.19.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
| [@rollup/rollup-linux-x64-gnu](https://rollupjs.org/)
([source](https://redirect.github.com/rollup/rollup)) | [`4.44.0` ->
`4.44.2`](https://renovatebot.com/diffs/npm/@rollup%2frollup-linux-x64-gnu/4.44.0/4.44.2)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@rollup%2frollup-linux-x64-gnu/4.44.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@rollup%2frollup-linux-x64-gnu/4.44.0/4.44.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| optionalDependencies | patch |
| [@swc/core](https://swc.rs)
([source](https://redirect.github.com/swc-project/swc)) | [`1.12.4` ->
`1.12.11`](https://renovatebot.com/diffs/npm/@swc%2fcore/1.12.4/1.12.11)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@swc%2fcore/1.12.11?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@swc%2fcore/1.12.4/1.12.11?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
|
[@types/bun](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/bun)
([source](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/bun))
| [`1.2.16` ->
`1.2.18`](https://renovatebot.com/diffs/npm/@types%2fbun/1.2.16/1.2.18)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@types%2fbun/1.2.18?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@types%2fbun/1.2.16/1.2.18?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
|
[@types/dockerode](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/dockerode)
([source](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/dockerode))
| [`3.3.41` ->
`3.3.42`](https://renovatebot.com/diffs/npm/@types%2fdockerode/3.3.41/3.3.42)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@types%2fdockerode/3.3.42?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@types%2fdockerode/3.3.41/3.3.42?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
|
[@types/lodash](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/lodash)
([source](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash))
| [`4.17.18` ->
`4.17.20`](https://renovatebot.com/diffs/npm/@types%2flodash/4.17.18/4.17.20)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@types%2flodash/4.17.20?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@types%2flodash/4.17.18/4.17.20?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
|
[@types/node](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node)
([source](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node))
| [`22.15.32` ->
`22.16.3`](https://renovatebot.com/diffs/npm/@types%2fnode/22.15.32/22.16.3)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@types%2fnode/22.16.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@types%2fnode/22.15.32/22.16.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
|
[@typescript-eslint/eslint-plugin](https://typescript-eslint.io/packages/eslint-plugin)
([source](https://redirect.github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin))
| [`8.34.1` ->
`8.36.0`](https://renovatebot.com/diffs/npm/@typescript-eslint%2feslint-plugin/8.34.1/8.36.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@typescript-eslint%2feslint-plugin/8.36.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@typescript-eslint%2feslint-plugin/8.34.1/8.36.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
|
[@vueuse/components](https://redirect.github.com/vueuse/vueuse/tree/main/packages/components#readme)
([source](https://redirect.github.com/vueuse/vueuse/tree/HEAD/packages/components))
| [`13.4.0` ->
`13.5.0`](https://renovatebot.com/diffs/npm/@vueuse%2fcomponents/13.4.0/13.5.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@vueuse%2fcomponents/13.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vueuse%2fcomponents/13.4.0/13.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | minor |
| [@vueuse/core](https://redirect.github.com/vueuse/vueuse)
([source](https://redirect.github.com/vueuse/vueuse/tree/HEAD/packages/core))
| [`13.4.0` ->
`13.5.0`](https://renovatebot.com/diffs/npm/@vueuse%2fcore/13.4.0/13.5.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@vueuse%2fcore/13.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vueuse%2fcore/13.4.0/13.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
| [@vueuse/core](https://redirect.github.com/vueuse/vueuse)
([source](https://redirect.github.com/vueuse/vueuse/tree/HEAD/packages/core))
| [`13.4.0` ->
`13.5.0`](https://renovatebot.com/diffs/npm/@vueuse%2fcore/13.4.0/13.5.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@vueuse%2fcore/13.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vueuse%2fcore/13.4.0/13.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | minor |
|
[@vueuse/integrations](https://redirect.github.com/vueuse/vueuse/tree/main/packages/integrations#readme)
([source](https://redirect.github.com/vueuse/vueuse/tree/HEAD/packages/integrations))
| [`13.4.0` ->
`13.5.0`](https://renovatebot.com/diffs/npm/@vueuse%2fintegrations/13.4.0/13.5.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@vueuse%2fintegrations/13.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vueuse%2fintegrations/13.4.0/13.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | minor |
|
[@vueuse/nuxt](https://redirect.github.com/vueuse/vueuse/tree/main/packages/nuxt#readme)
([source](https://redirect.github.com/vueuse/vueuse/tree/HEAD/packages/nuxt))
| [`13.4.0` ->
`13.5.0`](https://renovatebot.com/diffs/npm/@vueuse%2fnuxt/13.4.0/13.5.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@vueuse%2fnuxt/13.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vueuse%2fnuxt/13.4.0/13.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
|
[awalsh128/cache-apt-pkgs-action](https://redirect.github.com/awalsh128/cache-apt-pkgs-action)
| `v1.4.3` -> `v1.5.1` |
[![age](https://developer.mend.io/api/mc/badges/age/github-tags/awalsh128%2fcache-apt-pkgs-action/v1.5.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/github-tags/awalsh128%2fcache-apt-pkgs-action/v1.4.3/v1.5.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| action | minor |
| [cache-manager](https://redirect.github.com/jaredwray/cacheable)
([source](https://redirect.github.com/jaredwray/cacheable/tree/HEAD/packages/cache-manager))
| [`7.0.0` ->
`7.0.1`](https://renovatebot.com/diffs/npm/cache-manager/7.0.0/7.0.1) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/cache-manager/7.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/cache-manager/7.0.0/7.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
|
[commit-and-tag-version](https://redirect.github.com/absolute-version/commit-and-tag-version)
| [`9.5.0` ->
`9.6.0`](https://renovatebot.com/diffs/npm/commit-and-tag-version/9.5.0/9.6.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/commit-and-tag-version/9.6.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/commit-and-tag-version/9.5.0/9.6.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
|
[concurrently](https://redirect.github.com/open-cli-tools/concurrently)
| [`9.1.2` ->
`9.2.0`](https://renovatebot.com/diffs/npm/concurrently/9.1.2/9.2.0) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/concurrently/9.2.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/concurrently/9.1.2/9.2.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
| [dotenv](https://redirect.github.com/motdotla/dotenv) | [`17.1.0` ->
`17.2.0`](https://renovatebot.com/diffs/npm/dotenv/17.1.0/17.2.0) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/dotenv/17.2.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/dotenv/17.1.0/17.2.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | minor |
| [eslint](https://eslint.org)
([source](https://redirect.github.com/eslint/eslint)) | [`9.29.0` ->
`9.30.1`](https://renovatebot.com/diffs/npm/eslint/9.29.0/9.30.1) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/eslint/9.30.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/eslint/9.29.0/9.30.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
|
[eslint-plugin-import](https://redirect.github.com/import-js/eslint-plugin-import)
| [`2.31.0` ->
`2.32.0`](https://renovatebot.com/diffs/npm/eslint-plugin-import/2.31.0/2.32.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/eslint-plugin-import/2.32.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/eslint-plugin-import/2.31.0/2.32.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
|
[eslint-plugin-n](https://redirect.github.com/eslint-community/eslint-plugin-n)
| [`17.20.0` ->
`17.21.0`](https://renovatebot.com/diffs/npm/eslint-plugin-n/17.20.0/17.21.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/eslint-plugin-n/17.21.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/eslint-plugin-n/17.20.0/17.21.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
|
[eslint-plugin-prettier](https://redirect.github.com/prettier/eslint-plugin-prettier)
| [`5.5.0` ->
`5.5.1`](https://renovatebot.com/diffs/npm/eslint-plugin-prettier/5.5.0/5.5.1)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/eslint-plugin-prettier/5.5.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/eslint-plugin-prettier/5.5.0/5.5.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
| [eslint-plugin-vue](https://eslint.vuejs.org)
([source](https://redirect.github.com/vuejs/eslint-plugin-vue)) |
[`10.2.0` ->
`10.3.0`](https://renovatebot.com/diffs/npm/eslint-plugin-vue/10.2.0/10.3.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/eslint-plugin-vue/10.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/eslint-plugin-vue/10.2.0/10.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
| [fast-check](https://fast-check.dev/)
([source](https://redirect.github.com/dubzzz/fast-check/tree/HEAD/packages/fast-check))
| [`4.1.1` ->
`4.2.0`](https://renovatebot.com/diffs/npm/fast-check/4.1.1/4.2.0) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/fast-check/4.2.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/fast-check/4.1.1/4.2.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
| [glob](https://redirect.github.com/isaacs/node-glob) | [`11.0.1` ->
`11.0.3`](https://renovatebot.com/diffs/npm/glob/11.0.1/11.0.3) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/glob/11.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/glob/11.0.1/11.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [happy-dom](https://redirect.github.com/capricorn86/happy-dom) |
[`18.0.0` ->
`18.0.1`](https://renovatebot.com/diffs/npm/happy-dom/18.0.0/18.0.1) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/happy-dom/18.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/happy-dom/18.0.0/18.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
|
[inquirer](https://redirect.github.com/SBoudrias/Inquirer.js/blob/main/packages/inquirer/README.md)
([source](https://redirect.github.com/SBoudrias/Inquirer.js)) |
[`12.6.3` ->
`12.7.0`](https://renovatebot.com/diffs/npm/inquirer/12.6.3/12.7.0) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/inquirer/12.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/inquirer/12.6.3/12.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | minor |
|
[isomorphic-dompurify](https://redirect.github.com/kkomelin/isomorphic-dompurify)
| [`2.25.0` ->
`2.26.0`](https://renovatebot.com/diffs/npm/isomorphic-dompurify/2.25.0/2.26.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/isomorphic-dompurify/2.26.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/isomorphic-dompurify/2.25.0/2.26.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | minor |
| [lucide-vue-next](https://lucide.dev)
([source](https://redirect.github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-vue-next))
| [`0.519.0` ->
`0.525.0`](https://renovatebot.com/diffs/npm/lucide-vue-next/0.519.0/0.525.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/lucide-vue-next/0.525.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/lucide-vue-next/0.519.0/0.525.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | minor |
|
[marked-base-url](https://redirect.github.com/markedjs/marked-base-url)
| [`1.1.6` ->
`1.1.7`](https://renovatebot.com/diffs/npm/marked-base-url/1.1.6/1.1.7)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/marked-base-url/1.1.7?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/marked-base-url/1.1.6/1.1.7?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [node](https://nodejs.org)
([source](https://redirect.github.com/nodejs/node)) | `22.16.0` ->
`22.17.0` |
[![age](https://developer.mend.io/api/mc/badges/age/node-version/node/v22.17.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/node-version/node/v22.16.0/v22.17.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| | minor |
| [nuxt](https://nuxt.com)
([source](https://redirect.github.com/nuxt/nuxt/tree/HEAD/packages/nuxt))
| [`3.17.5` ->
`3.17.6`](https://renovatebot.com/diffs/npm/nuxt/3.17.5/3.17.6) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/nuxt/3.17.6?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/nuxt/3.17.5/3.17.6?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
| [pnpm](https://pnpm.io)
([source](https://redirect.github.com/pnpm/pnpm/tree/HEAD/pnpm)) |
[`10.12.4` ->
`10.13.1`](https://renovatebot.com/diffs/npm/pnpm/10.12.4/10.13.1) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/pnpm/10.13.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/pnpm/10.12.4/10.13.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| packageManager | minor |
| [pnpm](https://pnpm.io)
([source](https://redirect.github.com/pnpm/pnpm/tree/HEAD/pnpm)) |
[`10.12.4` ->
`10.13.1`](https://renovatebot.com/diffs/npm/pnpm/10.12.4/10.13.1) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/pnpm/10.13.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/pnpm/10.12.4/10.13.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| engines | minor |
| [prettier](https://prettier.io)
([source](https://redirect.github.com/prettier/prettier)) | [`3.5.3` ->
`3.6.2`](https://renovatebot.com/diffs/npm/prettier/3.5.3/3.6.2) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/prettier/3.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/prettier/3.5.3/3.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
|
[prettier-plugin-tailwindcss](https://redirect.github.com/tailwindlabs/prettier-plugin-tailwindcss)
| [`0.6.13` ->
`0.6.14`](https://renovatebot.com/diffs/npm/prettier-plugin-tailwindcss/0.6.13/0.6.14)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/prettier-plugin-tailwindcss/0.6.14?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/prettier-plugin-tailwindcss/0.6.13/0.6.14?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
| [reka-ui](https://redirect.github.com/unovue/reka-ui) | [`2.3.1` ->
`2.3.2`](https://renovatebot.com/diffs/npm/reka-ui/2.3.1/2.3.2) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/reka-ui/2.3.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/reka-ui/2.3.1/2.3.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [semver](https://redirect.github.com/npm/node-semver) | [`7.7.1` ->
`7.7.2`](https://renovatebot.com/diffs/npm/semver/7.7.1/7.7.2) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/semver/7.7.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/semver/7.7.1/7.7.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [systeminformation](https://systeminformation.io)
([source](https://redirect.github.com/sebhildebrandt/systeminformation))
| [`5.27.6` ->
`5.27.7`](https://renovatebot.com/diffs/npm/systeminformation/5.27.6/5.27.7)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/systeminformation/5.27.7?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/systeminformation/5.27.6/5.27.7?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [tsx](https://tsx.is)
([source](https://redirect.github.com/privatenumber/tsx)) | [`4.19.3` ->
`4.20.3`](https://renovatebot.com/diffs/npm/tsx/4.19.3/4.20.3) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/tsx/4.20.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/tsx/4.19.3/4.20.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | minor |
|
[typescript-eslint](https://typescript-eslint.io/packages/typescript-eslint)
([source](https://redirect.github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint))
| [`8.34.1` ->
`8.36.0`](https://renovatebot.com/diffs/npm/typescript-eslint/8.34.1/8.36.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/typescript-eslint/8.36.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/typescript-eslint/8.34.1/8.36.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
| [vite](https://vite.dev)
([source](https://redirect.github.com/vitejs/vite/tree/HEAD/packages/vite))
| [`7.0.3` ->
`7.0.4`](https://renovatebot.com/diffs/npm/vite/7.0.3/7.0.4) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/vite/7.0.4?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vite/7.0.3/7.0.4?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
| [vitest](https://redirect.github.com/vitest-dev/vitest)
([source](https://redirect.github.com/vitest-dev/vitest/tree/HEAD/packages/vitest))
| [`3.0.7` ->
`3.2.4`](https://renovatebot.com/diffs/npm/vitest/3.0.7/3.2.4) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/vitest/3.2.4?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vitest/3.0.7/3.2.4?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
|
[vue-i18n](https://redirect.github.com/intlify/vue-i18n/tree/master/packages/vue-i18n#readme)
([source](https://redirect.github.com/intlify/vue-i18n/tree/HEAD/packages/vue-i18n))
| [`11.1.6` ->
`11.1.9`](https://renovatebot.com/diffs/npm/vue-i18n/11.1.6/11.1.9) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/vue-i18n/11.1.9?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vue-i18n/11.1.6/11.1.9?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [vue-sonner](https://redirect.github.com/xiaoluoboding/vue-sonner) |
[`1.3.0` ->
`1.3.2`](https://renovatebot.com/diffs/npm/vue-sonner/1.3.0/1.3.2) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/vue-sonner/1.3.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vue-sonner/1.3.0/1.3.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [vuetify](https://vuetifyjs.com)
([source](https://redirect.github.com/vuetifyjs/vuetify/tree/HEAD/packages/vuetify))
| [`3.8.10` ->
`3.9.0`](https://renovatebot.com/diffs/npm/vuetify/3.8.10/3.9.0) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/vuetify/3.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vuetify/3.8.10/3.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | minor |
| [wrangler](https://redirect.github.com/cloudflare/workers-sdk)
([source](https://redirect.github.com/cloudflare/workers-sdk/tree/HEAD/packages/wrangler))
| [`^3.87.0` ->
`^3.114.10`](https://renovatebot.com/diffs/npm/wrangler/3.114.10/3.114.11)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/wrangler/3.114.11?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/wrangler/3.114.10/3.114.11?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
| [ws](https://redirect.github.com/websockets/ws) | [`8.18.2` ->
`8.18.3`](https://renovatebot.com/diffs/npm/ws/8.18.2/8.18.3) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/ws/8.18.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/ws/8.18.2/8.18.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
| [ws](https://redirect.github.com/websockets/ws) | [`8.18.2` ->
`8.18.3`](https://renovatebot.com/diffs/npm/ws/8.18.2/8.18.3) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/ws/8.18.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/ws/8.18.2/8.18.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [zod](https://zod.dev)
([source](https://redirect.github.com/colinhacks/zod)) | [`3.24.2` ->
`3.25.76`](https://renovatebot.com/diffs/npm/zod/3.24.2/3.25.76) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/zod/3.25.76?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/zod/3.24.2/3.25.76?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | minor |
| [zod](https://zod.dev)
([source](https://redirect.github.com/colinhacks/zod)) | [`3.25.67` ->
`3.25.76`](https://renovatebot.com/diffs/npm/zod/3.25.67/3.25.76) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/zod/3.25.76?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/zod/3.25.67/3.25.76?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [zx](https://google.github.io/zx/)
([source](https://redirect.github.com/google/zx)) | [`8.3.2` ->
`8.6.2`](https://renovatebot.com/diffs/npm/zx/8.3.2/8.6.2) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/zx/8.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/zx/8.3.2/8.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | minor |
| [zx](https://google.github.io/zx/)
([source](https://redirect.github.com/google/zx)) | [`8.5.5` ->
`8.6.2`](https://renovatebot.com/diffs/npm/zx/8.5.5/8.6.2) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/zx/8.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/zx/8.5.5/8.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |

---

### Release Notes

<details>
<summary>eslint/eslint (@&#8203;eslint/js)</summary>

###
[`v9.30.1`](https://redirect.github.com/eslint/eslint/compare/v9.30.0...b3dbc16563cb7036d75edff9814e17053a645321)

[Compare
Source](https://redirect.github.com/eslint/eslint/compare/v9.30.0...v9.30.1)

###
[`v9.30.0`](https://redirect.github.com/eslint/eslint/compare/v9.29.0...5a5d5261037fdf84a91f2f22d3726d58572453f4)

[Compare
Source](https://redirect.github.com/eslint/eslint/compare/v9.29.0...v9.30.0)

</details>

<details>
<summary>faker-js/faker (@&#8203;faker-js/faker)</summary>

###
[`v9.9.0`](https://redirect.github.com/faker-js/faker/blob/HEAD/CHANGELOG.md#990-2025-07-01)

[Compare
Source](https://redirect.github.com/faker-js/faker/compare/v9.8.0...v9.9.0)

##### New Locales

- **locale:** add word data to pt\_br and pt\_pt locales
([#&#8203;3531](https://redirect.github.com/faker-js/faker/issues/3531))
([a405ac8](a405ac8740))

##### Features

- **location:** simple coordinate methods
([#&#8203;3528](https://redirect.github.com/faker-js/faker/issues/3528))
([d07d96d](d07d96d018))

</details>

<details>
<summary>floating-ui/floating-ui (@&#8203;floating-ui/dom)</summary>

###
[`v1.7.2`](https://redirect.github.com/floating-ui/floating-ui/blob/HEAD/packages/dom/CHANGELOG.md#172)

[Compare
Source](https://redirect.github.com/floating-ui/floating-ui/compare/@floating-ui/dom@1.7.1...@floating-ui/dom@1.7.2)

##### Patch Changes

- perf: reduce memory allocations
- Update dependencies: `@floating-ui/utils@0.2.10`,
`@floating-ui/core@1.7.2`

</details>

<details>
<summary>floating-ui/floating-ui (@&#8203;floating-ui/utils)</summary>

###
[`v0.2.10`](https://redirect.github.com/floating-ui/floating-ui/blob/HEAD/packages/utils/CHANGELOG.md#0210)

[Compare
Source](https://redirect.github.com/floating-ui/floating-ui/compare/@floating-ui/utils@0.2.9...@floating-ui/utils@0.2.10)

##### Patch Changes

- refactor: small performance improvements
- perf: reduce memory allocations

</details>

<details>
<summary>floating-ui/floating-ui (@&#8203;floating-ui/vue)</summary>

###
[`v1.1.7`](https://redirect.github.com/floating-ui/floating-ui/blob/HEAD/packages/vue/CHANGELOG.md#117)

[Compare
Source](https://redirect.github.com/floating-ui/floating-ui/compare/@floating-ui/vue@1.1.6...@floating-ui/vue@1.1.7)

##### Patch Changes

- Update dependencies: `@floating-ui/utils@0.2.10`,
`@floating-ui/dom@1.7.2`

</details>

<details>
<summary>dotansimha/graphql-code-generator
(@&#8203;graphql-codegen/client-preset)</summary>

###
[`v4.8.3`](https://redirect.github.com/dotansimha/graphql-code-generator/blob/HEAD/packages/presets/client/CHANGELOG.md#483)

[Compare
Source](https://redirect.github.com/dotansimha/graphql-code-generator/compare/@graphql-codegen/client-preset@4.8.2...@graphql-codegen/client-preset@4.8.3)

##### Patch Changes

-
[#&#8203;10362](https://redirect.github.com/dotansimha/graphql-code-generator/pull/10362)
[`3188b8c`](3188b8c39e)
Thanks [@&#8203;Brookke](https://redirect.github.com/Brookke)! - Make
generated type compatible with noImplicitOverride=true

-
[#&#8203;10373](https://redirect.github.com/dotansimha/graphql-code-generator/pull/10373)
[`c3295f9`](c3295f9c60)
Thanks [@&#8203;eddeee888](https://redirect.github.com/eddeee888)! - Fix
client preset not working with exactOptionalPropertyTypes=true when
documentMode=string

- Updated dependencies
\[[`3188b8c`](3188b8c39e),
[`c3295f9`](c3295f9c60)]:
-
[@&#8203;graphql-codegen/typed-document-node](https://redirect.github.com/graphql-codegen/typed-document-node)@&#8203;5.1.2

</details>

<details>
<summary>dotansimha/graphql-code-generator
(@&#8203;graphql-codegen/typed-document-node)</summary>

###
[`v5.1.2`](https://redirect.github.com/dotansimha/graphql-code-generator/blob/HEAD/packages/plugins/typescript/typed-document-node/CHANGELOG.md#512)

[Compare
Source](https://redirect.github.com/dotansimha/graphql-code-generator/compare/@graphql-codegen/typed-document-node@5.1.1...@graphql-codegen/typed-document-node@5.1.2)

##### Patch Changes

-
[#&#8203;10362](https://redirect.github.com/dotansimha/graphql-code-generator/pull/10362)
[`3188b8c`](3188b8c39e)
Thanks [@&#8203;Brookke](https://redirect.github.com/Brookke)! - Make
generated type compatible with noImplicitOverride=true

-
[#&#8203;10373](https://redirect.github.com/dotansimha/graphql-code-generator/pull/10373)
[`c3295f9`](c3295f9c60)
Thanks [@&#8203;eddeee888](https://redirect.github.com/eddeee888)! - Fix
client preset not working with exactOptionalPropertyTypes=true when
documentMode=string

</details>

<details>
<summary>ianvs/prettier-plugin-sort-imports
(@&#8203;ianvs/prettier-plugin-sort-imports)</summary>

###
[`v4.5.1`](https://redirect.github.com/ianvs/prettier-plugin-sort-imports/compare/v4.5.0...040fa5e3a7dd01a90d80bb12072344745e426da6)

[Compare
Source](https://redirect.github.com/ianvs/prettier-plugin-sort-imports/compare/v4.5.0...v4.5.1)

###
[`v4.5.0`](https://redirect.github.com/ianvs/prettier-plugin-sort-imports/compare/v4.4.2...3497e9a87974954e42198d04d69d9a2a24dbebbd)

[Compare
Source](https://redirect.github.com/ianvs/prettier-plugin-sort-imports/compare/v4.4.2...v4.5.0)

</details>

<details>
<summary>nuxt/devtools (@&#8203;nuxt/devtools)</summary>

###
[`v2.6.2`](https://redirect.github.com/nuxt/devtools/blob/HEAD/CHANGELOG.md#262-2025-07-02)

[Compare
Source](https://redirect.github.com/nuxt/devtools/compare/v2.6.1...v2.6.2)

##### Bug Fixes

- panel dragging issue, close
[#&#8203;874](https://redirect.github.com/nuxt/devtools/issues/874),
close
[#&#8203;871](https://redirect.github.com/nuxt/devtools/issues/871),
close
[#&#8203;873](https://redirect.github.com/nuxt/devtools/issues/873)
([619de37](619de37ace))

###
[`v2.6.1`](https://redirect.github.com/nuxt/devtools/blob/HEAD/CHANGELOG.md#261-2025-07-01)

[Compare
Source](https://redirect.github.com/nuxt/devtools/compare/v2.6.0...v2.6.1)

##### Bug Fixes

- **deps:** do not depend on `@nuxt/schema`
([#&#8203;872](https://redirect.github.com/nuxt/devtools/issues/872))
([62443ec](62443ecb12))

###
[`v2.6.0`](https://redirect.github.com/nuxt/devtools/blob/HEAD/CHANGELOG.md#260-2025-06-29)

[Compare
Source](https://redirect.github.com/nuxt/devtools/compare/v2.5.0...v2.6.0)

##### Bug Fixes

- timing labels wrapping
([#&#8203;866](https://redirect.github.com/nuxt/devtools/issues/866))
([fd01e60](fd01e6022a))

##### Features

- update deps
([eef2c09](eef2c09ea1))

</details>

<details>
<summary>nuxt/eslint (@&#8203;nuxt/eslint)</summary>

###
[`v1.5.2`](https://redirect.github.com/nuxt/eslint/releases/tag/v1.5.2)

[Compare
Source](https://redirect.github.com/nuxt/eslint/compare/v1.5.1...v1.5.2)

#####    🚀 Features

- Add option `features.import.plugin` to swap plugin implementation,
close [#&#8203;587](https://redirect.github.com/nuxt/eslint/issues/587)
 -  by [@&#8203;antfu](https://redirect.github.com/antfu) in
[https://github.com/nuxt/eslint/issues/587](https://redirect.github.com/nuxt/eslint/issues/587)
[<samp>(66f5e)</samp>](https://redirect.github.com/nuxt/eslint/commit/66f5ee0)

#####     [View changes on
GitHub](https://redirect.github.com/nuxt/eslint/compare/v1.5.1...v1.5.2)

###
[`v1.5.1`](https://redirect.github.com/nuxt/eslint/releases/tag/v1.5.1)

[Compare
Source](https://redirect.github.com/nuxt/eslint/compare/v1.5.0...v1.5.1)

#####    🐞 Bug Fixes

- **eslint-config**: Replace deprecated vue/object-property-newline
option  -  by [@&#8203;amery](https://redirect.github.com/amery) in
[https://github.com/nuxt/eslint/issues/586](https://redirect.github.com/nuxt/eslint/issues/586)
[<samp>(7805e)</samp>](https://redirect.github.com/nuxt/eslint/commit/7805e0d)

#####     [View changes on
GitHub](https://redirect.github.com/nuxt/eslint/compare/v1.5.0...v1.5.1)

###
[`v1.5.0`](https://redirect.github.com/nuxt/eslint/releases/tag/v1.5.0)

[Compare
Source](https://redirect.github.com/nuxt/eslint/compare/v1.4.1...v1.5.0)

#####    🚀 Features

- Switch to `eslint-plugin-import-lite`, update deps  -  by
[@&#8203;antfu](https://redirect.github.com/antfu)
[<samp>(31bd8)</samp>](https://redirect.github.com/nuxt/eslint/commit/31bd8a0)

#####    🐞 Bug Fixes

- **eslint-config**: Add file type restrictions to prevent CSS parsing
errors  -  by [@&#8203;amery](https://redirect.github.com/amery) in
[https://github.com/nuxt/eslint/issues/584](https://redirect.github.com/nuxt/eslint/issues/584)
[<samp>(40521)</samp>](https://redirect.github.com/nuxt/eslint/commit/40521a1)

#####     [View changes on
GitHub](https://redirect.github.com/nuxt/eslint/compare/v1.4.1...v1.5.0)

</details>

<details>
<summary>nuxt/test-utils (@&#8203;nuxt/test-utils)</summary>

###
[`v3.19.2`](https://redirect.github.com/nuxt/test-utils/releases/tag/v3.19.2)

[Compare
Source](https://redirect.github.com/nuxt/test-utils/compare/v3.19.1...v3.19.2)

> 3.19.2 is the next patch release.
>
> **Timetable**: 1 July

#### 👉 Changelog

[compare
changes](https://redirect.github.com/nuxt/test-utils/compare/v3.19.1...v3.19.2)

##### 🩹 Fixes

- **config:** Add missing mocks for vue-devtools
([#&#8203;1321](https://redirect.github.com/nuxt/test-utils/pull/1321))
- **runtime-utils:** Prevent event duplication
([#&#8203;1328](https://redirect.github.com/nuxt/test-utils/pull/1328))
- **config:** Include tests without `.nuxt.` extension
([#&#8203;1311](https://redirect.github.com/nuxt/test-utils/pull/1311))
- **deps:** Drop `@nuxt/schema` dependeny
([fa3a99b4](https://redirect.github.com/nuxt/test-utils/commit/fa3a99b4))
- **config:** Use 'projects' for `vitest` >= v3.2
([#&#8203;1344](https://redirect.github.com/nuxt/test-utils/pull/1344))
- **module:** Use user `vite` version to merge config
([#&#8203;1345](https://redirect.github.com/nuxt/test-utils/pull/1345))
- **runtime-utils:** Handle computed defined using an object
([#&#8203;1342](https://redirect.github.com/nuxt/test-utils/pull/1342))

##### 🏡 Chore

- Prefer `nuxt` over `nuxi`
([#&#8203;1310](https://redirect.github.com/nuxt/test-utils/pull/1310))
- Pin node types
([93921643](https://redirect.github.com/nuxt/test-utils/commit/93921643))
- Do not include dev-deps in `engines.node` calculation
([2f74359b](https://redirect.github.com/nuxt/test-utils/commit/2f74359b))
- Add type assertions for indexed access
([51b4a4e3](https://redirect.github.com/nuxt/test-utils/commit/51b4a4e3))
- Update installed-check flag
([2b97d885](https://redirect.github.com/nuxt/test-utils/commit/2b97d885))

#####  Tests

- Update stub name for nuxt v4
([e7b07843](https://redirect.github.com/nuxt/test-utils/commit/e7b07843))
- Satisfy typescript
([fb0dea24](https://redirect.github.com/nuxt/test-utils/commit/fb0dea24))
- Update cucumber test for nuxt v4 welcome screen template
([8ec7782f](https://redirect.github.com/nuxt/test-utils/commit/8ec7782f))
- Simplify test
([90278bac](https://redirect.github.com/nuxt/test-utils/commit/90278bac))
- Update workspace example
([02f9b0a0](https://redirect.github.com/nuxt/test-utils/commit/02f9b0a0))
- Make browser tests forward-compat with v4
([574ea5f9](https://redirect.github.com/nuxt/test-utils/commit/574ea5f9))

##### 🤖 CI

- Remove forced corepack installation
([bf19bd3a](https://redirect.github.com/nuxt/test-utils/commit/bf19bd3a))
- Run `knip`
([819aeacc](https://redirect.github.com/nuxt/test-utils/commit/819aeacc))
- Prepare environment before knipping
([ec7d8ddd](https://redirect.github.com/nuxt/test-utils/commit/ec7d8ddd))

##### ❤️ Contributors

- Daniel Roe
([@&#8203;danielroe](https://redirect.github.com/danielroe))
- Tomina ([@&#8203;Thomaash](https://redirect.github.com/Thomaash))
- lutejka ([@&#8203;lutejka](https://redirect.github.com/lutejka))
- J-Michalek
([@&#8203;J-Michalek](https://redirect.github.com/J-Michalek))

</details>

<details>
<summary>rollup/rollup (@&#8203;rollup/rollup-linux-x64-gnu)</summary>

###
[`v4.44.2`](https://redirect.github.com/rollup/rollup/blob/HEAD/CHANGELOG.md#4442)

[Compare
Source](https://redirect.github.com/rollup/rollup/compare/v4.44.1...v4.44.2)

*2025-07-04*

##### Bug Fixes

- Correctly handle `@__PURE__` annotations after `new` keyword
([#&#8203;5998](https://redirect.github.com/rollup/rollup/issues/5998))
- Generate correct source mapping for closing braces of block statements
([#&#8203;5999](https://redirect.github.com/rollup/rollup/issues/5999))

##### Pull Requests

- [#&#8203;5998](https://redirect.github.com/rollup/rollup/pull/5998):
Support `@__PURE__` when nested after new in constructor invocations
([@&#8203;TrickyPi](https://redirect.github.com/TrickyPi))
- [#&#8203;5999](https://redirect.github.com/rollup/rollup/pull/5999):
Add location info for closing brace of block statement
([@&#8203;TrickyPi](https://redirect.github.com/TrickyPi))
- [#&#8203;6002](https://redirect.github.com/rollup/rollup/pull/6002):
chore(deps): update dependency vite to v7
([@&#8203;renovate](https://redirect.github.com/renovate)\[bot],
[@&#8203;lukastaegert](https://redirect.github.com/lukastaegert))
- [#&#8203;6004](https://redirect.github.com/rollup/rollup/pull/6004):
fix(deps): lock file maintenance minor/patch updates
([@&#8203;renovate](https://redirect.github.com/renovate)\[bot],
[@&#8203;lukastaegert](https://redirect.github.com/lukastaegert))

###
[`v4.44.1`](https://redirect.github.com/rollup/rollup/blob/HEAD/CHANGELOG.md#4441)

[Compare
Source](https://redirect.github.com/rollup/rollup/compare/v4.44.0...v4.44.1)

*2025-06-26*

##### Bug Fixes

- Reinstate maxParallelFileOps limit of 1000 to resolve the issue for
some
([#&#8203;5992](https://redirect.github.com/rollup/rollup/issues/5992))

##### Pull Requests

- [#&#8203;5988](https://redirect.github.com/rollup/rollup/pull/5988):
fix(deps): lock file maintenance minor/patch updates
([@&#8203;renovate](https://redirect.github.com/renovate)\[bot],
[@&#8203;lukastaegert](https://redirect.github.com/lukastaegert))
- [#&#8203;5992](https://redirect.github.com/rollup/rollup/pull/5992):
Set maxParallelFileOps to 1000
([@&#8203;lukastaegert](https://redirect.github.com/lukastaegert))

</details>

<details>
<summary>swc-project/swc (@&#8203;swc/core)</summary>

###
[`v1.12.11`](https://redirect.github.com/swc-project/swc/blob/HEAD/CHANGELOG.md#11211---2025-07-08)

[Compare
Source](https://redirect.github.com/swc-project/swc/compare/v1.12.9...v1.12.11)

##### Bug Fixes

- **(ci)** Fix CI
([#&#8203;10790](https://redirect.github.com/swc-project/swc/issues/10790))
([b3f9760](b3f97604b8))

- **(es)** Use `default-features = false` for `swc` crate usages
([#&#8203;10776](https://redirect.github.com/swc-project/swc/issues/10776))
([50b2eac](50b2eacdf7))

- **(es)** Make `swc_typescript` optional
([#&#8203;10792](https://redirect.github.com/swc-project/swc/issues/10792))
([c32569d](c32569dd55))

- **(preset-env)** Fix `default` value for `caniuse`
([#&#8203;10754](https://redirect.github.com/swc-project/swc/issues/10754))
([aa4cd5b](aa4cd5ba7c))

- **(preset-env)** Revert `default` value
([#&#8203;10778](https://redirect.github.com/swc-project/swc/issues/10778))
([7af5824](7af58242c2))

##### Features

- **(es/minifeir)** Inline lazily initialized literals
([#&#8203;10752](https://redirect.github.com/swc-project/swc/issues/10752))
([fd5d2e2](fd5d2e2f33))

- **(es/minifier)** Evaluate `Number.XXX` constants
([#&#8203;10756](https://redirect.github.com/swc-project/swc/issues/10756))
([c47dab5](c47dab5f90))

- **(es/minifier)** Implement partial evaluation of array join
([#&#8203;10758](https://redirect.github.com/swc-project/swc/issues/10758))
([bdf3a98](bdf3a98bb4))

- **(swc\_core)** Expose `swc_ecma_parser/unstable`
([#&#8203;10744](https://redirect.github.com/swc-project/swc/issues/10744))
([db0679e](db0679e5ca))

##### Miscellaneous Tasks

- **(common)** Remove `clone()` in proc macro
([#&#8203;10762](https://redirect.github.com/swc-project/swc/issues/10762))
([12e3180](12e318036c))

- **(deps)** Update `browserslist-rs` to `0.19`
([#&#8203;10750](https://redirect.github.com/swc-project/swc/issues/10750))
([f8bf21c](f8bf21c072))

- **(deps)** Remove unused deps with cargo-shear
([#&#8203;10765](https://redirect.github.com/swc-project/swc/issues/10765))
([f4e4974](f4e4974ffe))

- **(es/module)** Drop `node` feature of `swc_ecma_loader`
([#&#8203;10761](https://redirect.github.com/swc-project/swc/issues/10761))
([44471b5](44471b5151))

- **(plugin/runner)** Remove unused feature and dependency
([#&#8203;10764](https://redirect.github.com/swc-project/swc/issues/10764))
([a7d8a0a](a7d8a0ac89))

##### Performance

- **(es/lexer)** Use `bitflags` for `Syntax`
([#&#8203;10676](https://redirect.github.com/swc-project/swc/issues/10676))
([bf8c722](bf8c722e25))

- **(es/lexer)** Do not scan number if there's no underscore
([#&#8203;10788](https://redirect.github.com/swc-project/swc/issues/10788))
([f5d92ee](f5d92ee1bf))

- **(es/lints)** Make rules not parallel
([#&#8203;10772](https://redirect.github.com/swc-project/swc/issues/10772))
([4e6001c](4e6001c5a4))

- **(es/lints)** Merge critical rules
([#&#8203;10773](https://redirect.github.com/swc-project/swc/issues/10773))
([816e75a](816e75a209))

- **(es/parser)** Reduce the number of context set ops
([#&#8203;10742](https://redirect.github.com/swc-project/swc/issues/10742))
([08b4e8b](08b4e8b285))

- **(es/parser)** Reduce value set operations for context
([#&#8203;10751](https://redirect.github.com/swc-project/swc/issues/10751))
([4976b12](4976b12f93))

- **(es/parser)** Reduce query ops of current token
([#&#8203;10766](https://redirect.github.com/swc-project/swc/issues/10766))
([4304f91](4304f9129c))

- **(es/parser)** Remove useless call in `parse_ident`
([#&#8203;10770](https://redirect.github.com/swc-project/swc/issues/10770))
([4ca12c9](4ca12c9725))

- **(es/renamer)** Reduce time complexity in case of conflict
([#&#8203;10749](https://redirect.github.com/swc-project/swc/issues/10749))
([0279914](02799141bf))

- **(hstr)** Do not compare string during creating atoms
([#&#8203;10791](https://redirect.github.com/swc-project/swc/issues/10791))
([43a4f11](43a4f117cb))

- Replace `rayon` with `par-iter`
([#&#8203;10774](https://redirect.github.com/swc-project/swc/issues/10774))
([a6e6ebe](a6e6ebeaca))

##### Refactor

- **(es)** Make `swc_ecma_lint` optional for `swc` crate
([#&#8203;10767](https://redirect.github.com/swc-project/swc/issues/10767))
([f80415b](f80415baa6))

- **(es/lexer)** Use const fn in `SyntaxFlags`
([#&#8203;10737](https://redirect.github.com/swc-project/swc/issues/10737))
([b9eb23a](b9eb23aec3))

- **(es/parser)** Cleanup `parse_setter_param`
([#&#8203;10745](https://redirect.github.com/swc-project/swc/issues/10745))
([70734f4](70734f40d4))

- **(es/parser)** Cleanup `typed-arena`
([#&#8203;10769](https://redirect.github.com/swc-project/swc/issues/10769))
([ce5138d](ce5138d3aa))

- **(es/parser)** Cleanup for ctx
([#&#8203;10777](https://redirect.github.com/swc-project/swc/issues/10777))
([d60a611](d60a611dc7))

- **(es/parser)** Delete `with_ctx`
([#&#8203;10779](https://redirect.github.com/swc-project/swc/issues/10779))
([ce057c5](ce057c55ef))

- **(es/parser)** Cleanup
([#&#8203;10781](https://redirect.github.com/swc-project/swc/issues/10781))
([176ce36](176ce36d24))

- **(es/preset)** Remove deprecated `preset_env` function and `feature`
module
([#&#8203;10759](https://redirect.github.com/swc-project/swc/issues/10759))
([fa0e0ab](fa0e0abf41))

- **(es/preset-env)** Use phf for corejs3 entry
([#&#8203;10712](https://redirect.github.com/swc-project/swc/issues/10712))
([658b26d](658b26d838))

##### Testing

- **(es/minifier)** Update the terser test list
([#&#8203;10748](https://redirect.github.com/swc-project/swc/issues/10748))
([1eace01](1eace01303))

- **(es/minifier)** Update the passing test list
([#&#8203;10782](https://redirect.github.com/swc-project/swc/issues/10782))
([8aa888b](8aa888bc2a))

- **(es/parser)** Add a test for duplicate labels
([#&#8203;10784](https://redirect.github.com/swc-project/swc/issues/10784))
([28fc643](28fc64310c))

##### Pref

- **(hstr)** Do not compare static tag
([#&#8203;10771](https://redirect.github.com/swc-project/swc/issues/10771))
([5d3ce83](5d3ce83add))

###
[`v1.12.9`](https://redirect.github.com/swc-project/swc/blob/HEAD/CHANGELOG.md#1129---2025-07-01)

[Compare
Source](https://redirect.github.com/swc-project/swc/compare/v1.12.7...v1.12.9)

##### Bug Fixes

- **(es/lexer)** Parse uppercase hex numbers correctly
([#&#8203;10728](https://redirect.github.com/swc-project/swc/issues/10728))
([ead6256](ead62560b0))

- **(es/lexer)** Allow keywords as jsx attribute names
([#&#8203;10730](https://redirect.github.com/swc-project/swc/issues/10730))
([04ef20a](https://redirect.github.com/swc-project/swc/commit/04ef20ad9b

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/unraid/api).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4yMy4yIiwidXBkYXRlZEluVmVyIjoiNDEuMjMuMiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-11 14:40:42 -04:00
renovate[bot]
2ed1308e40 chore(deps): update dependency vite-plugin-vue-tracer to v1 (#1472)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
|
[vite-plugin-vue-tracer](https://redirect.github.com/antfu/vite-plugin-vue-tracer)
| [`0.1.4` ->
`1.0.0`](https://renovatebot.com/diffs/npm/vite-plugin-vue-tracer/0.1.4/1.0.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/vite-plugin-vue-tracer/1.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vite-plugin-vue-tracer/0.1.4/1.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>antfu/vite-plugin-vue-tracer (vite-plugin-vue-tracer)</summary>

###
[`v1.0.0`](https://redirect.github.com/antfu/vite-plugin-vue-tracer/releases/tag/v1.0.0)

[Compare
Source](https://redirect.github.com/antfu/vite-plugin-vue-tracer/compare/v0.1.5...v1.0.0)

*No significant changes*

#####     [View changes on
GitHub](https://redirect.github.com/antfu/vite-plugin-vue-tracer/compare/v0.1.5...v1.0.0)

###
[`v0.1.5`](https://redirect.github.com/antfu/vite-plugin-vue-tracer/releases/tag/v0.1.5)

[Compare
Source](https://redirect.github.com/antfu/vite-plugin-vue-tracer/compare/v0.1.4...v0.1.5)

#####    🚀 Features

- Support Vite 7  -  by
[@&#8203;antfu](https://redirect.github.com/antfu)
[<samp>(6927e)</samp>](https://redirect.github.com/antfu/vite-plugin-vue-tracer/commit/6927e8a)

#####     [View changes on
GitHub](https://redirect.github.com/antfu/vite-plugin-vue-tracer/compare/v0.1.4...v0.1.5)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/unraid/api).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNy4yIiwidXBkYXRlZEluVmVyIjoiNDEuMjMuMiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-11 14:17:33 -04:00
Zack Spear
6c03df2b97 tests: server store trial extensions (#1504)
Requested in feature PR
https://github.com/unraid/api/pull/1490#issuecomment-3059002854

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Enhanced trial expiration messaging to clearly communicate when the
trial is expiring, options for extension, and the consequences of
expiration.
* Added dynamic display of trial extension options and actions based on
eligibility and time remaining before expiration.

* **Bug Fixes**
* Improved accuracy of messages and actions related to trial extension
eligibility and renewal windows.

* **Tests**
* Added comprehensive tests for trial extension eligibility, renewal
windows, and related user messages and actions.

* **Documentation**
* Updated English locale strings to reflect new trial expiration and
extension messages.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-11 14:17:09 -04:00
Pujit Mehrotra
074370c42c fix: over-eager cloud query from web components (#1506)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved initialization logic to ensure cloud state is only loaded
when the connect plugin is installed, enhancing reliability during
startup.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-11 14:16:53 -04:00
Zack Spear
f34a33bc9f feat: trial extension allowed within 5 days of expiration (#1490) 2025-07-10 17:21:24 -07:00
github-actions[bot]
c7801a9236 chore(main): release 4.9.5 (#1503)
🤖 I have created a release *beep* *boop*
---


## [4.9.5](https://github.com/unraid/api/compare/v4.9.4...v4.9.5)
(2025-07-10)


### Bug Fixes

* **connect:** rm eager restart on `ERROR_RETYING` connection status
([#1502](https://github.com/unraid/api/issues/1502))
([dd759d9](dd759d9f0f))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-10 10:27:45 -04:00
Pujit Mehrotra
dd759d9f0f fix(connect): rm eager restart on ERROR_RETYING connection status (#1502)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved connection handling to prevent unnecessary reconnection
attempts during error retry states, ensuring reconnections only occur on
specific failures.

* **Tests**
* Added comprehensive tests to verify connection recovery,
identity-based connection, logout behavior, DDoS prevention, and edge
case handling for connection state changes.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-10 10:21:54 -04:00
github-actions[bot]
74da8d81ef chore(main): release 4.9.4 (#1498)
🤖 I have created a release *beep* *boop*
---


## [4.9.4](https://github.com/unraid/api/compare/v4.9.3...v4.9.4)
(2025-07-09)


### Bug Fixes

* backport `<unraid-modals>` upon plg install when necessary
([#1499](https://github.com/unraid/api/issues/1499))
([33e0b1a](33e0b1ab24))
* DefaultPageLayout patch rollback omits legacy header logo
([#1497](https://github.com/unraid/api/issues/1497))
([ea20d1e](ea20d1e211))
* event emitter setup for writing status
([#1496](https://github.com/unraid/api/issues/1496))
([ca4e2db](ca4e2db1f2))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-09 13:40:56 -04:00
Pujit Mehrotra
33e0b1ab24 fix: backport <unraid-modals> upon plg install when necessary (#1499)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Prevented duplicate insertion of the modal component in the page
layout.

* **Chores**
* Improved installation script to ensure the modal component is added
only if missing.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-09 13:32:55 -04:00
Eli Bosley
ca4e2db1f2 fix: event emitter setup for writing status (#1496)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Chores**
* Updated permissions to allow additional Bash command patterns in the
configuration.
* Improved connection status updates by triggering them via event
listeners during application bootstrap.
* Adjusted module provider registrations to reflect service relocation
within the application structure.
* **Tests**
* Added comprehensive unit and integration tests for connection status
writing and cleanup behaviors.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-09 13:16:53 -04:00
Pujit Mehrotra
ea20d1e211 fix: DefaultPageLayout patch rollback omits legacy header logo (#1497)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Enhanced the header by displaying the OS version and additional server
information.
* Introduced a new notification system using a modern UI component for
toasts.
* Automatically creates a root session for local requests when no valid
session exists.

* **Bug Fixes**
* Removed outdated pop-up notification logic and bell icon from the
navigation area.

* **Style**
* Updated header layout and improved formatting for a cleaner
appearance.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-09 13:12:18 -04:00
400 changed files with 16431 additions and 11121 deletions

123
.claude/settings.json Normal file
View File

@@ -0,0 +1,123 @@
{
"permissions": {
"allow": [
"# Development Commands",
"Bash(pnpm install)",
"Bash(pnpm dev)",
"Bash(pnpm build)",
"Bash(pnpm test)",
"Bash(pnpm test:*)",
"Bash(pnpm lint)",
"Bash(pnpm lint:fix)",
"Bash(pnpm type-check)",
"Bash(pnpm codegen)",
"Bash(pnpm storybook)",
"Bash(pnpm --filter * dev)",
"Bash(pnpm --filter * build)",
"Bash(pnpm --filter * test)",
"Bash(pnpm --filter * lint)",
"Bash(pnpm --filter * codegen)",
"# Git Commands (read-only)",
"Bash(git status)",
"Bash(git diff)",
"Bash(git log)",
"Bash(git branch)",
"Bash(git remote -v)",
"# Search Commands",
"Bash(rg *)",
"# File System (read-only)",
"Bash(ls)",
"Bash(ls -la)",
"Bash(pwd)",
"Bash(find . -name)",
"Bash(find . -type)",
"# Node/NPM Commands",
"Bash(node --version)",
"Bash(pnpm --version)",
"Bash(npx --version)",
"# Environment Commands",
"Bash(echo $*)",
"Bash(which *)",
"# Process Commands",
"Bash(ps aux | grep)",
"Bash(lsof -i)",
"# Documentation Domains",
"WebFetch(domain:tailwindcss.com)",
"WebFetch(domain:github.com)",
"WebFetch(domain:reka-ui.com)",
"WebFetch(domain:nodejs.org)",
"WebFetch(domain:pnpm.io)",
"WebFetch(domain:vitejs.dev)",
"WebFetch(domain:nuxt.com)",
"WebFetch(domain:nestjs.com)",
"# IDE Integration",
"mcp__ide__getDiagnostics",
"# Browser MCP (for testing)",
"mcp__browsermcp__browser_navigate",
"mcp__browsermcp__browser_click",
"mcp__browsermcp__browser_screenshot"
],
"deny": [
"# Dangerous Commands",
"Bash(rm -rf)",
"Bash(chmod 777)",
"Bash(curl)",
"Bash(wget)",
"Bash(ssh)",
"Bash(scp)",
"Bash(sudo)",
"Bash(su)",
"Bash(pkill)",
"Bash(kill)",
"Bash(killall)",
"Bash(python)",
"Bash(python3)",
"Bash(pip)",
"Bash(npm)",
"Bash(yarn)",
"Bash(apt)",
"Bash(brew)",
"Bash(systemctl)",
"Bash(service)",
"Bash(docker)",
"Bash(docker-compose)",
"# File Modification (use Edit/Write tools instead)",
"Bash(sed)",
"Bash(awk)",
"Bash(perl)",
"Bash(echo > *)",
"Bash(echo >> *)",
"Bash(cat > *)",
"Bash(cat >> *)",
"Bash(tee)",
"# Git Write Commands (require explicit user action)",
"Bash(git add)",
"Bash(git commit)",
"Bash(git push)",
"Bash(git pull)",
"Bash(git merge)",
"Bash(git rebase)",
"Bash(git checkout)",
"Bash(git reset)",
"Bash(git clean)",
"# Package Management Write Commands",
"Bash(pnpm add)",
"Bash(pnpm remove)",
"Bash(pnpm update)",
"Bash(pnpm upgrade)"
]
},
"enableAllProjectMcpServers": false
}

View File

@@ -1,18 +0,0 @@
{
"permissions": {
"allow": [
"Bash(rg:*)",
"Bash(find:*)",
"Bash(pnpm codegen:*)",
"Bash(pnpm dev:*)",
"Bash(pnpm build:*)",
"Bash(pnpm test:*)",
"Bash(grep:*)",
"Bash(pnpm type-check:*)",
"Bash(pnpm lint:*)",
"Bash(pnpm --filter ./api lint)",
"Bash(mv:*)"
]
},
"enableAllProjectMcpServers": false
}

20
.github/CODEOWNERS vendored
View File

@@ -1,20 +0,0 @@
# Default owners for everything in the repo
* @elibosley @pujitm @mdatelle @zackspear
# API specific files
/api/ @elibosley @pujitm @mdatelle
# Web frontend files
/web/ @elibosley @mdatelle @zackspear
# Plugin related files
/plugin/ @elibosley
# Unraid UI specific files
/unraid-ui/ @mdatelle @zackspear @pujitm
# GitHub workflows and configuration
/.github/ @elibosley
# Documentation
*.md @elibosley @pujitm @mdatelle @zackspear

View File

@@ -0,0 +1,78 @@
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
# - "src/**/*.tsx"
# - "src/**/*.js"
# - "src/**/*.jsx"
jobs:
claude-review:
# Optional: Filter by PR author
# if: |
# github.event.pull_request.user.login == 'external-contributor' ||
# github.event.pull_request.user.login == 'new-developer' ||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@beta
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
# model: "claude-opus-4-20250514"
# Direct prompt for automated review (no @claude mention needed)
direct_prompt: |
Please review this pull request and provide feedback on:
- Code quality and best practices
- Potential bugs or issues
- Performance considerations
- Security concerns
- Test coverage
Be constructive and helpful in your feedback.
# Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR
# use_sticky_comment: true
# Optional: Customize review based on file types
# direct_prompt: |
# Review this PR focusing on:
# - For TypeScript files: Type safety and proper interface usage
# - For API endpoints: Security, input validation, and error handling
# - For React components: Performance, accessibility, and best practices
# - For tests: Coverage, edge cases, and test quality
# Optional: Different prompts for different authors
# direct_prompt: |
# ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' &&
# 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' ||
# 'Please provide a thorough code review focusing on our coding standards and best practices.' }}
# Optional: Add specific tools for running tests or linting
# allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)"
# Optional: Skip review for certain conditions
# if: |
# !contains(github.event.pull_request.title, '[skip-review]') &&
# !contains(github.event.pull_request.title, '[WIP]')

64
.github/workflows/claude.yml vendored Normal file
View File

@@ -0,0 +1,64 @@
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@beta
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
# model: "claude-opus-4-20250514"
# Optional: Customize the trigger phrase (default: @claude)
# trigger_phrase: "/claude"
# Optional: Trigger when specific user is assigned to an issue
# assignee_trigger: "claude-bot"
# Optional: Allow Claude to run specific commands
# allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
# Optional: Add custom instructions for Claude to customize its behavior for your project
# custom_instructions: |
# Follow our coding standards
# Ensure all new code has tests
# Use TypeScript for new files
# Optional: Custom environment variables for Claude
# claude_env: |
# NODE_ENV: test

View File

@@ -25,7 +25,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '22.17.1'
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -33,7 +33,7 @@ jobs:
run_install: false
- name: Cache APT Packages
uses: awalsh128/cache-apt-pkgs-action@v1.4.3
uses: awalsh128/cache-apt-pkgs-action@v1.5.1
with:
packages: bash procps python3 libvirt-dev jq zstd git build-essential libvirt-daemon-system
version: 1.0

View File

@@ -45,7 +45,7 @@ jobs:
node-version-file: ".nvmrc"
- name: Cache APT Packages
uses: awalsh128/cache-apt-pkgs-action@v1.4.3
uses: awalsh128/cache-apt-pkgs-action@v1.5.1
with:
packages: bash procps python3 libvirt-dev jq zstd git build-essential libvirt-daemon-system
version: 1.0
@@ -190,7 +190,7 @@ jobs:
${{ runner.os }}-pnpm-store-
- name: Cache APT Packages
uses: awalsh128/cache-apt-pkgs-action@v1.4.3
uses: awalsh128/cache-apt-pkgs-action@v1.5.1
with:
packages: bash procps python3 libvirt-dev jq zstd git build-essential
version: 1.0
@@ -267,7 +267,7 @@ jobs:
${{ runner.os }}-pnpm-store-
- name: Cache APT Packages
uses: awalsh128/cache-apt-pkgs-action@v1.4.3
uses: awalsh128/cache-apt-pkgs-action@v1.5.1
with:
packages: bash procps python3 libvirt-dev jq zstd git build-essential
version: 1.0

View File

@@ -30,7 +30,7 @@ jobs:
prerelease: false
- uses: actions/setup-node@v4
with:
node-version: '22.17.0'
node-version: '22.17.1'
- run: |
cat << 'EOF' > release-notes.txt
${{ steps.release-info.outputs.body }}

View File

@@ -31,7 +31,7 @@ jobs:
python-version: "3.13.5"
- name: Cache APT Packages
uses: awalsh128/cache-apt-pkgs-action@v1.4.3
uses: awalsh128/cache-apt-pkgs-action@v1.5.1
with:
packages: libvirt-dev
version: 1.0

3
.gitignore vendored
View File

@@ -109,3 +109,6 @@ plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/dat
# Config file that changes between versions
api/dev/Unraid.net/myservers.cfg
# Claude local settings
.claude/settings.local.json

2
.nvmrc
View File

@@ -1 +1 @@
22.16.0
22.17.1

View File

@@ -1 +1 @@
{".":"4.9.3"}
{".":"4.11.0"}

14
.vscode/settings.json vendored
View File

@@ -1,14 +0,0 @@
{
"files.associations": {
"*.page": "php"
},
"editor.codeActionsOnSave": {
"source.fixAll": "never",
"source.fixAll.eslint": "explicit"
},
"i18n-ally.localesPaths": ["locales"],
"i18n-ally.keystyle": "flat",
"eslint.experimental.useFlatConfig": true,
"typescript.preferences.importModuleSpecifier": "non-relative",
"javascript.preferences.importModuleSpecifier": "non-relative"
}

View File

@@ -1,22 +0,0 @@
{
"_comment": "rename this file to .vscode/sftp.json and replace name/host/privateKeyPath for your system",
"name": "Tower",
"host": "Tower.local",
"protocol": "sftp",
"port": 22,
"username": "root",
"privateKeyPath": "C:/Users/username/.ssh/tower",
"remotePath": "/",
"context": "plugin/source/dynamix.unraid.net/",
"uploadOnSave": true,
"useTempFile": false,
"openSsh": false,
"ignore": [
"// comment: ignore dot files/dirs in root of repo",
".github",
".vscode",
".git",
".DS_Store"
]
}

View File

@@ -0,0 +1,81 @@
@custom-variant dark (&:where(.dark, .dark *));
@layer utilities {
:host {
--tw-divide-y-reverse: 0;
--tw-border-style: solid;
--tw-font-weight: initial;
--tw-tracking: initial;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-translate-z: 0;
--tw-rotate-x: rotateX(0);
--tw-rotate-y: rotateY(0);
--tw-rotate-z: rotateZ(0);
--tw-skew-x: skewX(0);
--tw-skew-y: skewY(0);
--tw-space-x-reverse: 0;
--tw-gradient-position: initial;
--tw-gradient-from: #0000;
--tw-gradient-via: #0000;
--tw-gradient-to: #0000;
--tw-gradient-stops: initial;
--tw-gradient-via-stops: initial;
--tw-gradient-from-position: 0%;
--tw-gradient-via-position: 50%;
--tw-gradient-to-position: 100%;
--tw-shadow: 0 0 #0000;
--tw-shadow-color: initial;
--tw-inset-shadow: 0 0 #0000;
--tw-inset-shadow-color: initial;
--tw-ring-color: initial;
--tw-ring-shadow: 0 0 #0000;
--tw-inset-ring-color: initial;
--tw-inset-ring-shadow: 0 0 #0000;
--tw-ring-inset: initial;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-offset-shadow: 0 0 #0000;
--tw-blur: initial;
--tw-brightness: initial;
--tw-contrast: initial;
--tw-grayscale: initial;
--tw-hue-rotate: initial;
--tw-invert: initial;
--tw-opacity: initial;
--tw-saturate: initial;
--tw-sepia: initial;
--tw-drop-shadow: initial;
--tw-duration: initial;
--tw-ease: initial;
}
}
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: hsl(var(--border));
}
body {
--color-alpha: #1c1b1b;
--color-beta: #f2f2f2;
--color-gamma: #999999;
--color-gamma-opaque: rgba(153, 153, 153, 0.5);
--color-customgradient-start: rgba(242, 242, 242, 0);
--color-customgradient-end: rgba(242, 242, 242, 0.85);
--shadow-beta: 0 25px 50px -12px rgba(242, 242, 242, 0.15);
--ring-offset-shadow: 0 0 var(--color-beta);
--ring-shadow: 0 0 var(--color-beta);
}
button:not(:disabled),
[role='button']:not(:disabled) {
cursor: pointer;
}
}

View File

@@ -0,0 +1,130 @@
/* Hybrid theme system: Native CSS + Theme Store fallback */
@layer base {
/* Light mode defaults */
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--ring: 0 0% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
/* Dark mode */
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--ring: 0 0% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
/* Alternative class-based dark mode support for specific Unraid themes */
.dark[data-theme='black'],
.dark[data-theme='gray'] {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--border: 0 0% 14.9%;
}
/* For web components: inherit CSS variables from the host */
:host {
--background: inherit;
--foreground: inherit;
--muted: inherit;
--muted-foreground: inherit;
--popover: inherit;
--popover-foreground: inherit;
--card: inherit;
--card-foreground: inherit;
--border: inherit;
--input: inherit;
--primary: inherit;
--primary-foreground: inherit;
--secondary: inherit;
--secondary-foreground: inherit;
--accent: inherit;
--accent-foreground: inherit;
--destructive: inherit;
--destructive-foreground: inherit;
--ring: inherit;
--chart-1: inherit;
--chart-2: inherit;
--chart-3: inherit;
--chart-4: inherit;
--chart-5: inherit;
}
/* Class-based dark mode support for web components using :host-context */
:host-context(.dark) {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--ring: 0 0% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
/* Alternative class-based dark mode support for specific Unraid themes */
:host-context(.dark[data-theme='black']),
:host-context(.dark[data-theme='gray']) {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--border: 0 0% 14.9%;
}
}

View File

@@ -0,0 +1,5 @@
/* Tailwind Shared Styles - Single entry point for all shared CSS */
@import './css-variables.css';
@import './unraid-theme.css';
@import './base-utilities.css';
@import './sonner.css';

View File

@@ -662,4 +662,4 @@
.sonner-loader[data-visible='false'] {
opacity: 0;
transform: scale(0.8) translate(-50%, -50%);
}
}

View File

@@ -0,0 +1,259 @@
@theme static {
/* Breakpoints */
--breakpoint-xs: 30rem;
--breakpoint-2xl: 100rem;
--breakpoint-3xl: 120rem;
/* Container settings */
--container-center: true;
--container-padding: 2rem;
--container-screen-2xl: 1400px;
/* Font families */
--font-sans:
clear-sans, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
/* Grid template columns */
--grid-template-columns-settings: 35% 1fr;
/* Border color default */
--default-border-color: var(--color-border);
--ui-border-muted: hsl(var(--border));
--ui-radius: 0.5rem;
--ui-primary: var(--color-primary-500);
--ui-primary-hover: var(--color-primary-600);
--ui-primary-active: var(--color-primary-700);
/* Color palette */
--color-inherit: inherit;
--color-transparent: transparent;
--color-black: #1c1b1b;
--color-grey-darkest: #222;
--color-grey-darker: #606f7b;
--color-grey-dark: #383735;
--color-grey-mid: #999999;
--color-grey: #e0e0e0;
--color-grey-light: #dae1e7;
--color-grey-lighter: #f1f5f8;
--color-grey-lightest: #f2f2f2;
--color-white: #ffffff;
/* Unraid colors */
--color-yellow-accent: #e9bf41;
--color-orange-dark: #f15a2c;
--color-orange: #ff8c2f;
/* Unraid red palette */
--color-unraid-red: #e22828;
--color-unraid-red-50: #fef2f2;
--color-unraid-red-100: #ffe1e1;
--color-unraid-red-200: #ffc9c9;
--color-unraid-red-300: #fea3a3;
--color-unraid-red-400: #fc6d6d;
--color-unraid-red-500: #f43f3f;
--color-unraid-red-600: #e22828;
--color-unraid-red-700: #bd1818;
--color-unraid-red-800: #9c1818;
--color-unraid-red-900: #821a1a;
--color-unraid-red-950: #470808;
/* Unraid green palette */
--color-unraid-green: #63a659;
--color-unraid-green-50: #f5f9f4;
--color-unraid-green-100: #e7f3e5;
--color-unraid-green-200: #d0e6cc;
--color-unraid-green-300: #aad1a4;
--color-unraid-green-400: #7db474;
--color-unraid-green-500: #63a659;
--color-unraid-green-600: #457b3e;
--color-unraid-green-700: #396134;
--color-unraid-green-800: #314e2d;
--color-unraid-green-900: #284126;
--color-unraid-green-950: #122211;
/* Primary colors (orange) */
--color-primary-50: #fff7ed;
--color-primary-100: #ffedd5;
--color-primary-200: #fed7aa;
--color-primary-300: #fdba74;
--color-primary-400: #fb923c;
--color-primary-500: #ff6600;
--color-primary-600: #ea580c;
--color-primary-700: #c2410c;
--color-primary-800: #9a3412;
--color-primary-900: #7c2d12;
--color-primary-950: #431407;
/* Header colors */
--color-header-text-primary: var(--header-text-primary);
--color-header-text-secondary: var(--header-text-secondary);
--color-header-background-color: var(--header-background-color);
/* Legacy colors */
--color-alpha: var(--color-alpha);
--color-beta: var(--color-beta);
--color-gamma: var(--color-gamma);
--color-gamma-opaque: var(--color-gamma-opaque);
--color-customgradient-start: var(--color-customgradient-start);
--color-customgradient-end: var(--color-customgradient-end);
/* Gradients */
--color-header-gradient-start: var(--header-gradient-start);
--color-header-gradient-end: var(--header-gradient-end);
--color-banner-gradient: var(--banner-gradient);
/* Font sizes */
--font-10px: 10px;
--font-12px: 12px;
--font-14px: 14px;
--font-16px: 16px;
--font-18px: 18px;
--font-20px: 20px;
--font-24px: 24px;
--font-30px: 30px;
/* Spacing */
--spacing-4_5: 1.125rem;
--spacing--8px: -8px;
--spacing-2px: 2px;
--spacing-4px: 4px;
--spacing-6px: 6px;
--spacing-8px: 8px;
--spacing-10px: 10px;
--spacing-12px: 12px;
--spacing-14px: 14px;
--spacing-16px: 16px;
--spacing-20px: 20px;
--spacing-24px: 24px;
--spacing-28px: 28px;
--spacing-32px: 32px;
--spacing-36px: 36px;
--spacing-40px: 40px;
--spacing-64px: 64px;
--spacing-80px: 80px;
--spacing-90px: 90px;
--spacing-150px: 150px;
--spacing-160px: 160px;
--spacing-200px: 200px;
--spacing-260px: 260px;
--spacing-300px: 300px;
--spacing-310px: 310px;
--spacing-350px: 350px;
--spacing-448px: 448px;
--spacing-512px: 512px;
--spacing-640px: 640px;
--spacing-800px: 800px;
/* Width and Height values */
--width-36px: 36px;
--height-36px: 36px;
/* Min/Max widths */
--min-width-86px: 86px;
--min-width-160px: 160px;
--min-width-260px: 260px;
--min-width-300px: 300px;
--min-width-310px: 310px;
--min-width-350px: 350px;
--min-width-800px: 800px;
--max-width-86px: 86px;
--max-width-160px: 160px;
--max-width-260px: 260px;
--max-width-300px: 300px;
--max-width-310px: 310px;
--max-width-350px: 350px;
--max-width-640px: 640px;
--max-width-800px: 800px;
--max-width-1024px: 1024px;
/* Animations */
--animate-mark-2: mark-2 1.5s ease infinite;
--animate-mark-3: mark-3 1.5s ease infinite;
--animate-mark-6: mark-6 1.5s ease infinite;
--animate-mark-7: mark-7 1.5s ease infinite;
/* Radius */
--radius: 0.5rem;
/* Text Resizing */
--text-xs: 1.2rem; /* 12px at 10px base */
--text-sm: 1.4rem; /* 14px at 10px base */
--text-base: 1.6rem; /* 16px at 10px base */
--text-lg: 1.8rem; /* 18px at 10px base */
--text-xl: 2rem; /* 20px at 10px base */
--text-2xl: 2.4rem; /* 24px at 10px base */
--text-3xl: 3rem; /* 30px at 10px base */
--text-4xl: 3.6rem; /* 36px at 10px base */
--text-5xl: 4.8rem; /* 48px at 10px base */
--text-6xl: 6rem; /* 60px at 10px base */
--text-7xl: 7.2rem; /* 72px at 10px base */
--text-8xl: 9.6rem; /* 96px at 10px base */
--text-9xl: 12.8rem; /* 128px at 10px base */
--spacing: 0.4rem; /* 4px at 10px base */
}
/* Keyframes */
@keyframes mark-2 {
50% {
transform: translateY(-40px);
}
to {
transform: translateY(0);
}
}
@keyframes mark-3 {
50% {
transform: translateY(-62px);
}
to {
transform: translateY(0);
}
}
@keyframes mark-6 {
50% {
transform: translateY(40px);
}
to {
transform: translateY(0);
}
}
@keyframes mark-7 {
50% {
transform: translateY(62px);
}
to {
transform: translateY(0);
}
}
/* Theme colors that reference CSS variables */
@theme inline {
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-ring: hsl(var(--ring));
--color-chart-1: hsl(var(--chart-1, 12 76% 61%));
--color-chart-2: hsl(var(--chart-2, 173 58% 39%));
--color-chart-3: hsl(var(--chart-3, 197 37% 24%));
--color-chart-4: hsl(var(--chart-4, 43 74% 66%));
--color-chart-5: hsl(var(--chart-5, 27 87% 67%));
}

View File

@@ -46,6 +46,16 @@ cd api && pnpm codegen # Generate GraphQL types
pnpm unraid:deploy <SERVER_IP> # Deploy all to Unraid server
```
### Developer Tools
```bash
unraid-api developer # Interactive prompt for tools
unraid-api developer --sandbox true # Enable GraphQL sandbox
unraid-api developer --sandbox false # Disable GraphQL sandbox
unraid-api developer --enable-modal # Enable modal testing tool
unraid-api developer --disable-modal # Disable modal testing tool
```
## Architecture Notes
### API Structure (NestJS)
@@ -135,3 +145,8 @@ Enables GraphQL playground at `http://tower.local/graphql`
- Place all mock declarations at the top level
- Use factory functions for module mocks to avoid hoisting issues
- Clear mocks between tests to ensure isolation
## Development Memories
- We are using tailwind v4 we do not need a tailwind config anymore
- always search the internet for tailwind v4 documentation when making tailwind related style changes

View File

@@ -15,6 +15,7 @@ PATHS_ACTIVATION_BASE=./dev/activation
PATHS_PASSWD=./dev/passwd
PATHS_RCLONE_SOCKET=./dev/rclone-socket
PATHS_LOG_BASE=./dev/log # Where we store logs
PATHS_CONNECT_STATUS_FILE_PATH=./dev/connectStatus.json # Connect plugin status file
ENVIRONMENT="development"
NODE_ENV="development"
PORT="3001"

View File

@@ -4,54 +4,59 @@ import noRelativeImportPaths from 'eslint-plugin-no-relative-import-paths';
import prettier from 'eslint-plugin-prettier';
import tseslint from 'typescript-eslint';
export default tseslint.config(eslint.configs.recommended, ...tseslint.configs.recommended, {
plugins: {
'no-relative-import-paths': noRelativeImportPaths,
prettier: prettier,
import: importPlugin,
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
{
ignores: ['src/graphql/generated/client/**/*', 'src/**/**/dummy-process.js'],
},
rules: {
'@typescript-eslint/no-redundant-type-constituents': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/naming-convention': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-empty-object-type': 'off',
'no-use-before-define': ['off'],
'no-multiple-empty-lines': ['error', { max: 1, maxBOF: 0, maxEOF: 1 }],
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-unused-expressions': 'off',
'import/no-unresolved': 'off',
'import/no-absolute-path': 'off',
'import/prefer-default-export': 'off',
'no-relative-import-paths/no-relative-import-paths': [
'error',
{ allowSameFolder: false, rootDir: 'src', prefix: '@app' },
],
'prettier/prettier': 'error',
'import/extensions': [
'error',
'ignorePackages',
{
js: 'always',
ts: 'always',
},
],
'no-restricted-globals': [
'error',
{
name: '__dirname',
message: 'Use import.meta.url instead of __dirname in ESM',
},
{
name: '__filename',
message: 'Use import.meta.url instead of __filename in ESM',
},
],
'eol-last': ['error', 'always'],
},
ignores: ['src/graphql/generated/client/**/*'],
});
{
plugins: {
'no-relative-import-paths': noRelativeImportPaths,
prettier: prettier,
import: importPlugin,
},
rules: {
'@typescript-eslint/no-redundant-type-constituents': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/naming-convention': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-empty-object-type': 'off',
'no-use-before-define': ['off'],
'no-multiple-empty-lines': ['error', { max: 1, maxBOF: 0, maxEOF: 1 }],
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-unused-expressions': 'off',
'import/no-unresolved': 'off',
'import/no-absolute-path': 'off',
'import/prefer-default-export': 'off',
'no-relative-import-paths/no-relative-import-paths': [
'error',
{ allowSameFolder: false, rootDir: 'src', prefix: '@app' },
],
'prettier/prettier': 'error',
'import/extensions': [
'error',
'ignorePackages',
{
js: 'always',
ts: 'always',
},
],
'no-restricted-globals': [
'error',
{
name: '__dirname',
message: 'Use import.meta.url instead of __dirname in ESM',
},
{
name: '__filename',
message: 'Use import.meta.url instead of __filename in ESM',
},
],
'eol-last': ['error', 'always'],
},
}
);

View File

@@ -1,9 +0,0 @@
{
"eslint.lintTask.options": "--flag unstable_ts_config",
"eslint.options": {
"flags": ["unstable_ts_config"],
"overrideConfigFile": ".eslintrc.ts"
},
"typescript.preferences.importModuleSpecifier": "non-relative",
"javascript.preferences.importModuleSpecifier": "non-relative"
}

View File

@@ -1,5 +1,63 @@
# Changelog
## [4.11.0](https://github.com/unraid/api/compare/v4.10.0...v4.11.0) (2025-07-28)
### Features
* tailwind v4 ([#1522](https://github.com/unraid/api/issues/1522)) ([2c62e0a](https://github.com/unraid/api/commit/2c62e0ad09c56d2293b76d07833dfb142c898937))
* **web:** install and configure nuxt ui ([#1524](https://github.com/unraid/api/issues/1524)) ([407585c](https://github.com/unraid/api/commit/407585cd40c409175d8e7b861f8d61d8cabc11c9))
### Bug Fixes
* add missing breakpoints ([#1535](https://github.com/unraid/api/issues/1535)) ([f5352e3](https://github.com/unraid/api/commit/f5352e3a26a2766e85d19ffb5f74960c536b91b3))
* border color incorrect in tailwind ([#1544](https://github.com/unraid/api/issues/1544)) ([f14b74a](https://github.com/unraid/api/commit/f14b74af91783b08640c0949c51ba7f18508f06f))
* **connect:** omit extraneous fields during connect config validation ([#1538](https://github.com/unraid/api/issues/1538)) ([45bd736](https://github.com/unraid/api/commit/45bd73698b2bd534a8aff2c6ac73403de6c58561))
* **deps:** pin dependencies ([#1528](https://github.com/unraid/api/issues/1528)) ([a74d935](https://github.com/unraid/api/commit/a74d935b566dd7af1a21824c9b7ab562232f9d8b))
* **deps:** pin dependency @nuxt/ui to 3.2.0 ([#1532](https://github.com/unraid/api/issues/1532)) ([8279531](https://github.com/unraid/api/commit/8279531f2b86a78e81a77e6c037a0fb752e98062))
* **deps:** update all non-major dependencies ([#1510](https://github.com/unraid/api/issues/1510)) ([1a8da6d](https://github.com/unraid/api/commit/1a8da6d92b96d3afa2a8b42446b36f1ee98b64a0))
* **deps:** update all non-major dependencies ([#1520](https://github.com/unraid/api/issues/1520)) ([e2fa648](https://github.com/unraid/api/commit/e2fa648d1cf5a6cbe3e55c3f52c203d26bb4d526))
* inject Tailwind CSS into client entry point ([#1537](https://github.com/unraid/api/issues/1537)) ([86b6c4f](https://github.com/unraid/api/commit/86b6c4f85b7b30bb4a13d57450a76bf4c28a3fff))
* make settings grid responsive ([#1463](https://github.com/unraid/api/issues/1463)) ([9dfdb8d](https://github.com/unraid/api/commit/9dfdb8dce781fa662d6434ee432e4521f905ffa5))
* **notifications:** gracefully handle & mask invalid notifications ([#1529](https://github.com/unraid/api/issues/1529)) ([05056e7](https://github.com/unraid/api/commit/05056e7ca1702eb7bf6c507950460b6b15bf7916))
* truncate log files when they take up more than 5mb of space ([#1530](https://github.com/unraid/api/issues/1530)) ([0a18b38](https://github.com/unraid/api/commit/0a18b38008dd86a125cde7f684636d5dbb36f082))
* use async for primary file read/writes ([#1531](https://github.com/unraid/api/issues/1531)) ([23b2b88](https://github.com/unraid/api/commit/23b2b8846158a27d1c9808bce0cc1506779c4dc3))
## [4.10.0](https://github.com/unraid/api/compare/v4.9.5...v4.10.0) (2025-07-15)
### Features
* trial extension allowed within 5 days of expiration ([#1490](https://github.com/unraid/api/issues/1490)) ([f34a33b](https://github.com/unraid/api/commit/f34a33bc9f1a7e135d453d9d31888789bfc3f878))
### Bug Fixes
* delay `nginx:reload` file mod effect by 10 seconds ([#1512](https://github.com/unraid/api/issues/1512)) ([af33e99](https://github.com/unraid/api/commit/af33e999a0480a77e3e6b2aa833b17b38b835656))
* **deps:** update all non-major dependencies ([#1489](https://github.com/unraid/api/issues/1489)) ([53b05eb](https://github.com/unraid/api/commit/53b05ebe5e2050cb0916fcd65e8d41370aee0624))
* ensure no crash if emhttp state configs are missing ([#1514](https://github.com/unraid/api/issues/1514)) ([1a7d35d](https://github.com/unraid/api/commit/1a7d35d3f6972fd8aff58c17b2b0fb79725e660e))
* **my.servers:** improve DNS resolution robustness for backup server ([#1518](https://github.com/unraid/api/issues/1518)) ([eecd9b1](https://github.com/unraid/api/commit/eecd9b1017a63651d1dc782feaa224111cdee8b6))
* over-eager cloud query from web components ([#1506](https://github.com/unraid/api/issues/1506)) ([074370c](https://github.com/unraid/api/commit/074370c42cdecc4dbc58193ff518aa25735c56b3))
* replace myservers.cfg reads in UpdateFlashBackup.php ([#1517](https://github.com/unraid/api/issues/1517)) ([441e180](https://github.com/unraid/api/commit/441e1805c108a6c1cd35ee093246b975a03f8474))
* rm short-circuit in `rc.unraid-api` if plugin config dir is absent ([#1515](https://github.com/unraid/api/issues/1515)) ([29dcb7d](https://github.com/unraid/api/commit/29dcb7d0f088937cefc5158055f48680e86e5c36))
## [4.9.5](https://github.com/unraid/api/compare/v4.9.4...v4.9.5) (2025-07-10)
### Bug Fixes
* **connect:** rm eager restart on `ERROR_RETYING` connection status ([#1502](https://github.com/unraid/api/issues/1502)) ([dd759d9](https://github.com/unraid/api/commit/dd759d9f0f841b296f8083bc67c6cd3f7a69aa5b))
## [4.9.4](https://github.com/unraid/api/compare/v4.9.3...v4.9.4) (2025-07-09)
### Bug Fixes
* backport `<unraid-modals>` upon plg install when necessary ([#1499](https://github.com/unraid/api/issues/1499)) ([33e0b1a](https://github.com/unraid/api/commit/33e0b1ab24bedb6a2c7b376ea73dbe65bc3044be))
* DefaultPageLayout patch rollback omits legacy header logo ([#1497](https://github.com/unraid/api/issues/1497)) ([ea20d1e](https://github.com/unraid/api/commit/ea20d1e2116fcafa154090fee78b42ec5d9ba584))
* event emitter setup for writing status ([#1496](https://github.com/unraid/api/issues/1496)) ([ca4e2db](https://github.com/unraid/api/commit/ca4e2db1f29126a1fa3784af563832edda64b0ca))
## [4.9.3](https://github.com/unraid/api/compare/v4.9.2...v4.9.3) (2025-07-09)

View File

@@ -1,7 +1,7 @@
###########################################################
# Development/Build Image
###########################################################
FROM node:22.17.0-bookworm-slim AS development
FROM node:22.17.1-bookworm-slim AS development
# Install build tools and dependencies
RUN apt-get update -y && apt-get install -y \

View File

@@ -27,19 +27,13 @@ const config: CodegenConfig = {
},
},
generates: {
// Generate Types for Mothership GraphQL Client
'src/graphql/generated/client/': {
documents: './src/graphql/mothership/*.ts',
schema: {
[process.env.MOTHERSHIP_GRAPHQL_LINK as string]: {
headers: {
origin: 'https://forums.unraid.net',
},
},
},
// Generate Types for CLI Internal GraphQL Queries
'src/unraid-api/cli/generated/': {
documents: ['src/unraid-api/cli/queries/**/*.ts', 'src/unraid-api/cli/mutations/**/*.ts'],
schema: './generated-schema.graphql',
preset: 'client',
presetConfig: {
gqlTagName: 'graphql',
gqlTagName: 'gql',
},
config: {
useTypeImports: true,
@@ -47,21 +41,6 @@ const config: CodegenConfig = {
},
plugins: [{ add: { content: '/* eslint-disable */' } }],
},
'src/graphql/generated/client/validators.ts': {
schema: {
[process.env.MOTHERSHIP_GRAPHQL_LINK as string]: {
headers: {
origin: 'https://forums.unraid.net',
},
},
},
plugins: ['typescript-validation-schema', { add: { content: '/* eslint-disable */' } }],
config: {
importFrom: '@app/graphql/generated/client/graphql.js',
strictScalars: false,
schema: 'zod',
},
},
},
};

View File

@@ -1,10 +1,12 @@
{
"version": "4.8.0",
"version": "4.10.0",
"extraOrigins": [
"https://google.com",
"https://test.com"
],
"sandbox": true,
"ssoSubIds": [],
"plugins": ["unraid-api-plugin-connect"]
"plugins": [
"unraid-api-plugin-connect"
]
}

View File

@@ -1,16 +1,12 @@
{
"wanaccess": false,
"wanport": 0,
"wanaccess": true,
"wanport": 8443,
"upnpEnabled": false,
"apikey": "",
"localApiKey": "",
"email": "",
"username": "",
"avatar": "",
"regWizTime": "",
"accesstoken": "",
"idtoken": "",
"refreshtoken": "",
"dynamicRemoteAccessType": "DISABLED",
"ssoSubIds": []
"apikey": "_______________________BIG_API_KEY_HERE_________________________",
"localApiKey": "_______________________LOCAL_API_KEY_HERE_________________________",
"email": "test@example.com",
"username": "zspearmint",
"avatar": "https://via.placeholder.com/200",
"regWizTime": "1611175408732_0951-1653-3509-FBA155FA23C0",
"dynamicRemoteAccessType": "DISABLED"
}

View File

@@ -0,0 +1,11 @@
{
"createdAt": "2025-07-23T17:34:06.301Z",
"description": "Internal admin API key used by CLI commands for system operations",
"id": "fc91da7b-0284-46f4-9018-55aa9759fba9",
"key": "_______SUPER_SECRET_KEY_______",
"name": "CliInternal",
"permissions": [],
"roles": [
"ADMIN"
]
}

View File

@@ -62,10 +62,17 @@ Switch between production and staging environments.
### Developer Mode
```bash
unraid-api developer
unraid-api developer # Interactive prompt for tools
unraid-api developer --sandbox true # Enable GraphQL sandbox
unraid-api developer --sandbox false # Disable GraphQL sandbox
unraid-api developer --enable-modal # Enable modal testing tool
unraid-api developer --disable-modal # Disable modal testing tool
```
Configure developer features for the API (e.g., GraphQL sandbox).
Configure developer features for the API:
- **GraphQL Sandbox**: Enable/disable Apollo GraphQL sandbox at `/graphql`
- **Modal Testing Tool**: Enable/disable UI modal testing in the Unraid menu
## API Key Management

View File

@@ -4,13 +4,19 @@ The Unraid API provides a GraphQL interface that allows you to interact with you
## Enabling the GraphQL Sandbox
1. First, enable developer mode using the CLI:
1. Enable developer mode using the CLI:
```bash
unraid-api developer --sandbox true
```
Or use the interactive mode:
```bash
unraid-api developer
```
2. Follow the prompts to enable the sandbox. This will allow you to access the Apollo Sandbox interface.
2. Once enabled, you can access the Apollo Sandbox interface
3. Access the GraphQL playground by navigating to:

View File

@@ -226,27 +226,6 @@ type Share implements Node {
luksStatus: String
}
type AccessUrl {
type: URL_TYPE!
name: String
ipv4: URL
ipv6: URL
}
enum URL_TYPE {
LAN
WIREGUARD
WAN
MDNS
OTHER
DEFAULT
}
"""
A field whose value conforms to the standard URL format as specified in RFC3986: https://www.ietf.org/rfc/rfc3986.txt.
"""
scalar URL
type DiskPartition {
"""The name of the partition"""
name: String!
@@ -1490,6 +1469,27 @@ type Plugin {
hasCliModule: Boolean
}
type AccessUrl {
type: URL_TYPE!
name: String
ipv4: URL
ipv6: URL
}
enum URL_TYPE {
LAN
WIREGUARD
WAN
MDNS
OTHER
DEFAULT
}
"""
A field whose value conforms to the standard URL format as specified in RFC3986: https://www.ietf.org/rfc/rfc3986.txt.
"""
scalar URL
type AccessUrlObject {
ipv4: String
ipv6: String
@@ -1653,6 +1653,7 @@ type Query {
services: [Service!]!
shares: [Share!]!
vars: Vars!
isInitialSetup: Boolean!
"""Get information about all VMs on the system"""
vms: Vms!
@@ -1829,7 +1830,6 @@ type Subscription {
notificationAdded: Notification!
notificationsOverview: NotificationOverview!
ownerSubscription: Owner!
registrationSubscription: Registration!
serversSubscription: Server!
parityHistorySubscription: ParityCheck!
arraySubscription: UnraidArray!

View File

@@ -1,6 +1,6 @@
{
"name": "@unraid/api",
"version": "4.9.3",
"version": "4.11.0",
"main": "src/cli/index.ts",
"type": "module",
"corepack": {
@@ -10,7 +10,7 @@
"author": "Lime Technology, Inc. <unraid.net>",
"license": "GPL-2.0-or-later",
"engines": {
"pnpm": "10.12.4"
"pnpm": "10.13.1"
},
"scripts": {
"// Development": "",
@@ -28,9 +28,8 @@
"preunraid:deploy": "pnpm build",
"unraid:deploy": "./scripts/deploy-dev.sh",
"// GraphQL Codegen": "",
"codegen": "MOTHERSHIP_GRAPHQL_LINK='https://staging.mothership.unraid.net/ws' graphql-codegen --config codegen.ts -r dotenv/config './.env.staging'",
"codegen:watch": "DOTENV_CONFIG_PATH='./.env.staging' graphql-codegen --config codegen.ts --watch -r dotenv/config",
"codegen:local": "NODE_TLS_REJECT_UNAUTHORIZED=0 MOTHERSHIP_GRAPHQL_LINK='https://mothership.localhost/ws' graphql-codegen --config codegen.ts --watch",
"codegen": "graphql-codegen --config codegen.ts",
"codegen:watch": "graphql-codegen --config codegen.ts --watch",
"// Code Quality": "",
"lint": "eslint --config .eslintrc.ts src/",
"lint:fix": "eslint --fix --config .eslintrc.ts src/",
@@ -57,21 +56,21 @@
"@as-integrations/fastify": "2.1.1",
"@fastify/cookie": "11.0.2",
"@fastify/helmet": "13.0.1",
"@graphql-codegen/client-preset": "4.8.2",
"@graphql-codegen/client-preset": "4.8.3",
"@graphql-tools/load-files": "7.0.1",
"@graphql-tools/merge": "9.0.24",
"@graphql-tools/schema": "10.0.23",
"@graphql-tools/utils": "10.8.6",
"@graphql-tools/merge": "9.1.1",
"@graphql-tools/schema": "10.0.25",
"@graphql-tools/utils": "10.9.1",
"@jsonforms/core": "3.6.0",
"@nestjs/apollo": "13.1.0",
"@nestjs/cache-manager": "3.0.1",
"@nestjs/common": "11.1.3",
"@nestjs/common": "11.1.5",
"@nestjs/config": "4.0.2",
"@nestjs/core": "11.1.3",
"@nestjs/core": "11.1.5",
"@nestjs/event-emitter": "3.0.1",
"@nestjs/graphql": "13.1.0",
"@nestjs/passport": "11.0.5",
"@nestjs/platform-fastify": "11.1.3",
"@nestjs/platform-fastify": "11.1.5",
"@nestjs/schedule": "6.0.0",
"@nestjs/throttler": "6.4.0",
"@reduxjs/toolkit": "2.8.2",
@@ -82,7 +81,7 @@
"accesscontrol": "2.2.1",
"bycontract": "2.0.11",
"bytes": "3.1.2",
"cache-manager": "7.0.0",
"cache-manager": "7.0.1",
"cacheable-lookup": "7.0.0",
"camelcase-keys": "9.1.3",
"casbin": "5.38.0",
@@ -94,11 +93,11 @@
"command-exists": "1.2.9",
"convert": "5.12.0",
"cookie": "1.0.2",
"cron": "4.3.1",
"cron": "4.3.2",
"cross-fetch": "4.1.0",
"diff": "8.0.2",
"dockerode": "4.0.7",
"dotenv": "17.1.0",
"dotenv": "17.2.1",
"execa": "9.6.0",
"exit-hook": "4.0.0",
"fastify": "5.4.0",
@@ -112,16 +111,16 @@
"graphql-scalars": "1.24.2",
"graphql-subscriptions": "3.0.0",
"graphql-tag": "2.12.6",
"graphql-ws": "6.0.5",
"graphql-ws": "6.0.6",
"ini": "5.0.0",
"ip": "2.0.1",
"jose": "6.0.11",
"jose": "6.0.12",
"json-bigint-patch": "0.0.8",
"lodash-es": "4.17.21",
"multi-ini": "2.3.2",
"mustache": "4.2.0",
"nest-authz": "2.17.0",
"nest-commander": "3.17.0",
"nest-commander": "3.18.0",
"nestjs-pino": "4.4.0",
"node-cache": "5.1.2",
"node-window-polyfill": "1.0.4",
@@ -138,11 +137,11 @@
"rxjs": "7.8.2",
"semver": "7.7.2",
"strftime": "0.10.3",
"systeminformation": "5.27.6",
"systeminformation": "5.27.7",
"uuid": "11.1.0",
"ws": "8.18.2",
"ws": "8.18.3",
"zen-observable-ts": "1.1.0",
"zod": "3.25.67"
"zod": "3.25.76"
},
"peerDependencies": {
"unraid-api-plugin-connect": "workspace:*"
@@ -153,71 +152,73 @@
}
},
"devDependencies": {
"@eslint/js": "9.29.0",
"@eslint/js": "9.32.0",
"@graphql-codegen/add": "5.0.3",
"@graphql-codegen/cli": "5.0.7",
"@graphql-codegen/fragment-matcher": "5.1.0",
"@graphql-codegen/import-types-preset": "3.0.1",
"@graphql-codegen/typed-document-node": "5.1.1",
"@graphql-codegen/typed-document-node": "5.1.2",
"@graphql-codegen/typescript": "4.1.6",
"@graphql-codegen/typescript-operations": "4.6.1",
"@graphql-codegen/typescript-resolvers": "4.5.1",
"@graphql-typed-document-node/core": "3.2.0",
"@ianvs/prettier-plugin-sort-imports": "4.4.2",
"@nestjs/testing": "11.1.3",
"@ianvs/prettier-plugin-sort-imports": "4.5.1",
"@nestjs/testing": "11.1.5",
"@originjs/vite-plugin-commonjs": "1.0.3",
"@rollup/plugin-node-resolve": "16.0.1",
"@swc/core": "1.12.4",
"@swc/core": "1.13.2",
"@types/async-exit-hook": "2.0.2",
"@types/bytes": "3.1.5",
"@types/cli-table": "0.3.4",
"@types/command-exists": "1.2.3",
"@types/cors": "2.8.19",
"@types/dockerode": "3.3.41",
"@types/dockerode": "3.3.42",
"@types/graphql-fields": "1.3.9",
"@types/graphql-type-uuid": "0.2.6",
"@types/ini": "4.1.1",
"@types/ip": "1.1.3",
"@types/lodash": "4.17.18",
"@types/lodash": "4.17.20",
"@types/lodash-es": "4.17.12",
"@types/mustache": "4.2.6",
"@types/node": "22.15.32",
"@types/node": "22.16.5",
"@types/pify": "6.1.0",
"@types/semver": "7.7.0",
"@types/sendmail": "1.4.7",
"@types/stoppable": "1.1.3",
"@types/strftime": "0.9.8",
"@types/supertest": "^6.0.3",
"@types/uuid": "10.0.0",
"@types/ws": "8.18.1",
"@types/wtfnode": "0.7.3",
"@vitest/coverage-v8": "3.2.4",
"@vitest/ui": "3.2.4",
"commit-and-tag-version": "9.6.0",
"cz-conventional-changelog": "3.3.0",
"eslint": "9.29.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-n": "17.20.0",
"eslint": "9.32.0",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-n": "17.21.2",
"eslint-plugin-no-relative-import-paths": "1.6.1",
"eslint-plugin-prettier": "5.5.0",
"eslint-plugin-prettier": "5.5.3",
"graphql-codegen-typescript-validation-schema": "0.17.1",
"jiti": "2.4.2",
"jiti": "2.5.1",
"nodemon": "3.1.10",
"prettier": "3.5.3",
"prettier": "3.6.2",
"rollup-plugin-node-externals": "8.0.1",
"commit-and-tag-version": "9.5.0",
"supertest": "^7.1.4",
"tsx": "4.20.3",
"type-fest": "4.41.0",
"typescript": "5.8.3",
"typescript-eslint": "8.34.1",
"typescript-eslint": "8.38.0",
"unplugin-swc": "1.5.5",
"vite": "7.0.3",
"vite": "7.0.6",
"vite-plugin-node": "7.0.0",
"vite-tsconfig-paths": "5.1.4",
"vitest": "3.2.4",
"zx": "8.5.5"
"zx": "8.7.1"
},
"overrides": {
"eslint": {
"jiti": "2.4.2"
"jiti": "2.5.1"
},
"@as-integrations/fastify": {
"fastify": "$fastify"
@@ -228,5 +229,5 @@
}
},
"private": true,
"packageManager": "pnpm@10.12.4"
"packageManager": "pnpm@10.13.1"
}

View File

@@ -1,45 +0,0 @@
import { getAllowedOrigins } from '@app/common/allowed-origins.js';
import { store } from '@app/store/index.js';
import { loadConfigFile } from '@app/store/modules/config.js';
import { loadStateFiles } from '@app/store/modules/emhttp.js';
import 'reflect-metadata';
import { expect, test } from 'vitest';
test('Returns allowed origins', async () => {
// Load state files into store
await store.dispatch(loadStateFiles()).unwrap();
await store.dispatch(loadConfigFile()).unwrap();
// Get allowed origins
const allowedOrigins = getAllowedOrigins();
// Test that the result is an array
expect(Array.isArray(allowedOrigins)).toBe(true);
// Test that it contains the expected socket paths
expect(allowedOrigins).toContain('/var/run/unraid-notifications.sock');
expect(allowedOrigins).toContain('/var/run/unraid-php.sock');
expect(allowedOrigins).toContain('/var/run/unraid-cli.sock');
// Test that it contains the expected local URLs
expect(allowedOrigins).toContain('http://localhost:8080');
expect(allowedOrigins).toContain('https://localhost:4443');
// Test that it contains the expected connect URLs
expect(allowedOrigins).toContain('https://connect.myunraid.net');
expect(allowedOrigins).toContain('https://connect-staging.myunraid.net');
expect(allowedOrigins).toContain('https://dev-my.myunraid.net:4000');
// Test that it contains the extra origins from config
expect(allowedOrigins).toContain('https://google.com');
expect(allowedOrigins).toContain('https://test.com');
// Test that it contains some of the remote URLs
expect(allowedOrigins).toContain('https://tower.local:4443');
expect(allowedOrigins).toContain('https://192.168.1.150:4443');
// Test that there are no duplicates
expect(allowedOrigins.length).toBe(new Set(allowedOrigins).size);
});

View File

@@ -1,137 +0,0 @@
import { ConfigService } from '@nestjs/config';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ApiConfigPersistence } from '@app/unraid-api/config/api-config.module.js';
import { ConfigPersistenceHelper } from '@app/unraid-api/config/persistence.helper.js';
describe('ApiConfigPersistence', () => {
let service: ApiConfigPersistence;
let configService: ConfigService;
let persistenceHelper: ConfigPersistenceHelper;
beforeEach(() => {
configService = {
get: vi.fn(),
set: vi.fn(),
} as any;
persistenceHelper = {} as ConfigPersistenceHelper;
service = new ApiConfigPersistence(configService, persistenceHelper);
});
describe('convertLegacyConfig', () => {
it('should migrate sandbox from string "yes" to boolean true', () => {
const legacyConfig = {
local: { sandbox: 'yes' },
api: { extraOrigins: '' },
remote: { ssoSubIds: '' },
};
const result = service.convertLegacyConfig(legacyConfig);
expect(result.sandbox).toBe(true);
});
it('should migrate sandbox from string "no" to boolean false', () => {
const legacyConfig = {
local: { sandbox: 'no' },
api: { extraOrigins: '' },
remote: { ssoSubIds: '' },
};
const result = service.convertLegacyConfig(legacyConfig);
expect(result.sandbox).toBe(false);
});
it('should migrate extraOrigins from comma-separated string to array', () => {
const legacyConfig = {
local: { sandbox: 'no' },
api: { extraOrigins: 'https://example.com,https://test.com' },
remote: { ssoSubIds: '' },
};
const result = service.convertLegacyConfig(legacyConfig);
expect(result.extraOrigins).toEqual(['https://example.com', 'https://test.com']);
});
it('should filter out non-HTTP origins from extraOrigins', () => {
const legacyConfig = {
local: { sandbox: 'no' },
api: {
extraOrigins: 'https://example.com,invalid-origin,http://test.com,ftp://bad.com',
},
remote: { ssoSubIds: '' },
};
const result = service.convertLegacyConfig(legacyConfig);
expect(result.extraOrigins).toEqual(['https://example.com', 'http://test.com']);
});
it('should handle empty extraOrigins string', () => {
const legacyConfig = {
local: { sandbox: 'no' },
api: { extraOrigins: '' },
remote: { ssoSubIds: '' },
};
const result = service.convertLegacyConfig(legacyConfig);
expect(result.extraOrigins).toEqual([]);
});
it('should migrate ssoSubIds from comma-separated string to array', () => {
const legacyConfig = {
local: { sandbox: 'no' },
api: { extraOrigins: '' },
remote: { ssoSubIds: 'user1,user2,user3' },
};
const result = service.convertLegacyConfig(legacyConfig);
expect(result.ssoSubIds).toEqual(['user1', 'user2', 'user3']);
});
it('should handle empty ssoSubIds string', () => {
const legacyConfig = {
local: { sandbox: 'no' },
api: { extraOrigins: '' },
remote: { ssoSubIds: '' },
};
const result = service.convertLegacyConfig(legacyConfig);
expect(result.ssoSubIds).toEqual([]);
});
it('should handle undefined config sections', () => {
const legacyConfig = {};
const result = service.convertLegacyConfig(legacyConfig);
expect(result.sandbox).toBe(false);
expect(result.extraOrigins).toEqual([]);
expect(result.ssoSubIds).toEqual([]);
});
it('should handle complete migration with all fields', () => {
const legacyConfig = {
local: { sandbox: 'yes' },
api: { extraOrigins: 'https://app1.example.com,https://app2.example.com' },
remote: { ssoSubIds: 'sub1,sub2,sub3' },
};
const result = service.convertLegacyConfig(legacyConfig);
expect(result.sandbox).toBe(true);
expect(result.extraOrigins).toEqual([
'https://app1.example.com',
'https://app2.example.com',
]);
expect(result.ssoSubIds).toEqual(['sub1', 'sub2', 'sub3']);
});
});
});

View File

@@ -1,158 +0,0 @@
import 'reflect-metadata';
import { cloneDeep } from 'lodash-es';
import { expect, test } from 'vitest';
import { getWriteableConfig } from '@app/core/utils/files/config-file-normalizer.js';
import { initialState } from '@app/store/modules/config.js';
test('it creates a FLASH config with NO OPTIONAL values', () => {
const basicConfig = initialState;
const config = getWriteableConfig(basicConfig, 'flash');
expect(config).toMatchInlineSnapshot(`
{
"api": {
"extraOrigins": "",
"version": "",
},
"local": {
"sandbox": "no",
},
"remote": {
"accesstoken": "",
"apikey": "",
"avatar": "",
"dynamicRemoteAccessType": "DISABLED",
"email": "",
"idtoken": "",
"localApiKey": "",
"refreshtoken": "",
"regWizTime": "",
"ssoSubIds": "",
"upnpEnabled": "",
"username": "",
"wanaccess": "",
"wanport": "",
},
}
`);
});
test('it creates a MEMORY config with NO OPTIONAL values', () => {
const basicConfig = initialState;
const config = getWriteableConfig(basicConfig, 'memory');
expect(config).toMatchInlineSnapshot(`
{
"api": {
"extraOrigins": "",
"version": "",
},
"connectionStatus": {
"minigraph": "PRE_INIT",
"upnpStatus": "",
},
"local": {
"sandbox": "no",
},
"remote": {
"accesstoken": "",
"allowedOrigins": "/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000",
"apikey": "",
"avatar": "",
"dynamicRemoteAccessType": "DISABLED",
"email": "",
"idtoken": "",
"localApiKey": "",
"refreshtoken": "",
"regWizTime": "",
"ssoSubIds": "",
"upnpEnabled": "",
"username": "",
"wanaccess": "",
"wanport": "",
},
}
`);
});
test('it creates a FLASH config with OPTIONAL values', () => {
const basicConfig = cloneDeep(initialState);
// 2fa & t2fa should be ignored
basicConfig.remote['2Fa'] = 'yes';
basicConfig.local['2Fa'] = 'yes';
basicConfig.api.extraOrigins = 'myextra.origins';
basicConfig.remote.upnpEnabled = 'yes';
basicConfig.connectionStatus.upnpStatus = 'Turned On';
const config = getWriteableConfig(basicConfig, 'flash');
expect(config).toMatchInlineSnapshot(`
{
"api": {
"extraOrigins": "myextra.origins",
"version": "",
},
"local": {
"sandbox": "no",
},
"remote": {
"accesstoken": "",
"apikey": "",
"avatar": "",
"dynamicRemoteAccessType": "DISABLED",
"email": "",
"idtoken": "",
"localApiKey": "",
"refreshtoken": "",
"regWizTime": "",
"ssoSubIds": "",
"upnpEnabled": "yes",
"username": "",
"wanaccess": "",
"wanport": "",
},
}
`);
});
test('it creates a MEMORY config with OPTIONAL values', () => {
const basicConfig = cloneDeep(initialState);
// 2fa & t2fa should be ignored
basicConfig.remote['2Fa'] = 'yes';
basicConfig.local['2Fa'] = 'yes';
basicConfig.api.extraOrigins = 'myextra.origins';
basicConfig.remote.upnpEnabled = 'yes';
basicConfig.connectionStatus.upnpStatus = 'Turned On';
const config = getWriteableConfig(basicConfig, 'memory');
expect(config).toMatchInlineSnapshot(`
{
"api": {
"extraOrigins": "myextra.origins",
"version": "",
},
"connectionStatus": {
"minigraph": "PRE_INIT",
"upnpStatus": "Turned On",
},
"local": {
"sandbox": "no",
},
"remote": {
"accesstoken": "",
"allowedOrigins": "/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000",
"apikey": "",
"avatar": "",
"dynamicRemoteAccessType": "DISABLED",
"email": "",
"idtoken": "",
"localApiKey": "",
"refreshtoken": "",
"regWizTime": "",
"ssoSubIds": "",
"upnpEnabled": "yes",
"username": "",
"wanaccess": "",
"wanport": "",
},
}
`);
});

View File

@@ -0,0 +1,5 @@
/* eslint-disable no-undef */
// Dummy process for PM2 testing
setInterval(() => {
// Keep process alive
}, 1000);

View File

@@ -0,0 +1,216 @@
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { execa } from 'execa';
import pm2 from 'pm2';
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import { isUnraidApiRunning } from '@app/core/utils/pm2/unraid-api-running.js';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const PROJECT_ROOT = join(__dirname, '../../../../..');
const DUMMY_PROCESS_PATH = join(__dirname, 'dummy-process.js');
const CLI_PATH = join(PROJECT_ROOT, 'dist/cli.js');
const TEST_PROCESS_NAME = 'test-unraid-api';
// Shared PM2 connection state
let pm2Connected = false;
// Helper function to run CLI command (assumes CLI is built)
async function runCliCommand(command: string, options: any = {}) {
return await execa('node', [CLI_PATH, command], options);
}
// Helper to ensure PM2 connection is established
async function ensurePM2Connection() {
if (pm2Connected) return;
return new Promise<void>((resolve, reject) => {
pm2.connect((err) => {
if (err) {
reject(err);
return;
}
pm2Connected = true;
resolve();
});
});
}
// Helper to delete specific test processes (lightweight, reuses connection)
async function deleteTestProcesses() {
if (!pm2Connected) {
// No connection, nothing to clean up
return;
}
const deletePromise = new Promise<void>((resolve) => {
// Delete specific processes we might have created
const processNames = ['unraid-api', TEST_PROCESS_NAME];
let deletedCount = 0;
const deleteNext = () => {
if (deletedCount >= processNames.length) {
resolve();
return;
}
const processName = processNames[deletedCount];
pm2.delete(processName, (deleteErr) => {
// Ignore errors, process might not exist
deletedCount++;
deleteNext();
});
};
deleteNext();
});
const timeoutPromise = new Promise<void>((resolve) => {
setTimeout(() => resolve(), 3000); // 3 second timeout
});
return Promise.race([deletePromise, timeoutPromise]);
}
// Helper to ensure PM2 is completely clean (heavy cleanup with daemon kill)
async function cleanupAllPM2Processes() {
// First delete test processes if we have a connection
if (pm2Connected) {
await deleteTestProcesses();
}
return new Promise<void>((resolve) => {
// Always connect fresh for daemon kill (in case we weren't connected)
pm2.connect((err) => {
if (err) {
// If we can't connect, assume PM2 is not running
pm2Connected = false;
resolve();
return;
}
// Kill the daemon to ensure fresh state
pm2.killDaemon((killErr) => {
pm2.disconnect();
pm2Connected = false;
// Small delay to let PM2 fully shutdown
setTimeout(resolve, 500);
});
});
});
}
describe.skipIf(!!process.env.CI)('PM2 integration tests', () => {
beforeAll(async () => {
// Build the CLI if it doesn't exist (only for CLI tests)
if (!existsSync(CLI_PATH)) {
console.log('Building CLI for integration tests...');
try {
await execa('pnpm', ['build'], {
cwd: PROJECT_ROOT,
stdio: 'inherit',
timeout: 120000, // 2 minute timeout for build
});
} catch (error) {
console.error('Failed to build CLI:', error);
throw new Error(
'Cannot run CLI integration tests without built CLI. Run `pnpm build` first.'
);
}
}
// Only do a full cleanup once at the beginning
await cleanupAllPM2Processes();
}, 150000); // 2.5 minute timeout for setup
afterAll(async () => {
// Only do a full cleanup once at the end
await cleanupAllPM2Processes();
});
afterEach(async () => {
// Lightweight cleanup after each test - just delete our test processes
await deleteTestProcesses();
}, 5000); // 5 second timeout for cleanup
describe('isUnraidApiRunning function', () => {
it('should return false when PM2 is not running the unraid-api process', async () => {
const result = await isUnraidApiRunning();
expect(result).toBe(false);
});
it('should return true when PM2 has unraid-api process running', async () => {
// Ensure PM2 connection
await ensurePM2Connection();
// Start a dummy process with the name 'unraid-api'
await new Promise<void>((resolve, reject) => {
pm2.start(
{
script: DUMMY_PROCESS_PATH,
name: 'unraid-api',
},
(startErr) => {
if (startErr) return reject(startErr);
resolve();
}
);
});
// Give PM2 time to start the process
await new Promise((resolve) => setTimeout(resolve, 2000));
const result = await isUnraidApiRunning();
expect(result).toBe(true);
}, 30000);
it('should return false when unraid-api process is stopped', async () => {
// Ensure PM2 connection
await ensurePM2Connection();
// Start and then stop the process
await new Promise<void>((resolve, reject) => {
pm2.start(
{
script: DUMMY_PROCESS_PATH,
name: 'unraid-api',
},
(startErr) => {
if (startErr) return reject(startErr);
// Stop the process after starting
setTimeout(() => {
pm2.stop('unraid-api', (stopErr) => {
if (stopErr) return reject(stopErr);
resolve();
});
}, 1000);
}
);
});
await new Promise((resolve) => setTimeout(resolve, 1000));
const result = await isUnraidApiRunning();
expect(result).toBe(false);
}, 30000);
it('should handle PM2 connection errors gracefully', async () => {
// Set an invalid PM2_HOME to force connection failure
const originalPM2Home = process.env.PM2_HOME;
process.env.PM2_HOME = '/invalid/path/that/does/not/exist';
const result = await isUnraidApiRunning();
expect(result).toBe(false);
// Restore original PM2_HOME
if (originalPM2Home) {
process.env.PM2_HOME = originalPM2Home;
} else {
delete process.env.PM2_HOME;
}
}, 15000); // 15 second timeout to allow for the Promise.race timeout
});
});

View File

@@ -17,7 +17,6 @@ exports[`Returns paths 1`] = `
"myservers-base",
"myservers-config",
"myservers-config-states",
"myservers-env",
"myservers-keepalive",
"keyfile-base",
"machine-id",

View File

@@ -1,303 +0,0 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { pubsub, PUBSUB_CHANNEL } from '@app/core/pubsub.js';
import { store } from '@app/store/index.js';
import { MyServersConfigMemory } from '@app/types/my-servers-config.js';
describe.skip('config tests', () => {
// Mock dependencies
vi.mock('@app/core/pubsub.js', () => {
const mockPublish = vi.fn();
return {
pubsub: {
publish: mockPublish,
},
PUBSUB_CHANNEL: {
OWNER: 'OWNER',
SERVERS: 'SERVERS',
},
__esModule: true,
default: {
pubsub: {
publish: mockPublish,
},
PUBSUB_CHANNEL: {
OWNER: 'OWNER',
SERVERS: 'SERVERS',
},
},
};
});
// Get the mock function for pubsub.publish
const mockPublish = vi.mocked(pubsub.publish);
// Clear mock before each test
beforeEach(() => {
mockPublish.mockClear();
});
vi.mock('@app/mothership/graphql-client.js', () => ({
GraphQLClient: {
clearInstance: vi.fn(),
},
}));
vi.mock('@app/mothership/jobs/ping-timeout-jobs.js', () => ({
stopPingTimeoutJobs: vi.fn(),
}));
const createConfigMatcher = (specificValues: Partial<MyServersConfigMemory> = {}) => {
const defaultMatcher = {
api: expect.objectContaining({
extraOrigins: expect.any(String),
version: expect.any(String),
}),
connectionStatus: expect.objectContaining({
minigraph: expect.any(String),
upnpStatus: expect.any(String),
}),
local: expect.objectContaining({
sandbox: expect.any(String),
}),
nodeEnv: expect.any(String),
remote: expect.objectContaining({
accesstoken: expect.any(String),
allowedOrigins: expect.any(String),
apikey: expect.any(String),
avatar: expect.any(String),
dynamicRemoteAccessType: expect.any(String),
email: expect.any(String),
idtoken: expect.any(String),
localApiKey: expect.any(String),
refreshtoken: expect.any(String),
regWizTime: expect.any(String),
ssoSubIds: expect.any(String),
upnpEnabled: expect.any(String),
username: expect.any(String),
wanaccess: expect.any(String),
wanport: expect.any(String),
}),
status: expect.any(String),
};
return expect.objectContaining({
...defaultMatcher,
...specificValues,
});
};
// test('Before init returns default values for all fields', async () => {
// const state = store.getState().config;
// expect(state).toMatchSnapshot();
// }, 10_000);
test('After init returns values from cfg file for all fields', async () => {
const { loadConfigFile } = await import('@app/store/modules/config.js');
// Load cfg into store
await store.dispatch(loadConfigFile());
// Check if store has cfg contents loaded
const state = store.getState().config;
expect(state).toMatchObject(createConfigMatcher());
});
test('updateUserConfig merges in changes to current state', async () => {
const { loadConfigFile, updateUserConfig } = await import('@app/store/modules/config.js');
// Load cfg into store
await store.dispatch(loadConfigFile());
// Update store
store.dispatch(
updateUserConfig({
remote: { avatar: 'https://via.placeholder.com/200' },
})
);
const state = store.getState().config;
expect(state).toMatchObject(
createConfigMatcher({
remote: expect.objectContaining({
avatar: 'https://via.placeholder.com/200',
}),
})
);
});
test('loginUser updates state and publishes to pubsub', async () => {
const { loginUser } = await import('@app/store/modules/config.js');
const userInfo = {
email: 'test@example.com',
avatar: 'https://via.placeholder.com/200',
username: 'testuser',
apikey: 'test-api-key',
localApiKey: 'test-local-api-key',
};
await store.dispatch(loginUser(userInfo));
expect(pubsub.publish).toHaveBeenCalledWith(PUBSUB_CHANNEL.OWNER, {
owner: {
username: userInfo.username,
url: '',
avatar: userInfo.avatar,
},
});
const state = store.getState().config;
expect(state).toMatchObject(
createConfigMatcher({
remote: expect.objectContaining(userInfo),
})
);
});
test('logoutUser clears state and publishes to pubsub', async () => {
const { logoutUser } = await import('@app/store/modules/config.js');
await store.dispatch(logoutUser({ reason: 'test logout' }));
expect(pubsub.publish).toHaveBeenCalledWith(PUBSUB_CHANNEL.SERVERS, { servers: [] });
expect(pubsub.publish).toHaveBeenCalledWith(PUBSUB_CHANNEL.OWNER, {
owner: {
username: 'root',
url: '',
avatar: '',
},
});
// expect(stopPingTimeoutJobs).toHaveBeenCalled();
// expect(GraphQLClient.clearInstance).toHaveBeenCalled();
});
test('updateAccessTokens updates token fields', async () => {
const { updateAccessTokens } = await import('@app/store/modules/config.js');
const tokens = {
accesstoken: 'new-access-token',
refreshtoken: 'new-refresh-token',
idtoken: 'new-id-token',
};
store.dispatch(updateAccessTokens(tokens));
const state = store.getState().config;
expect(state).toMatchObject(
createConfigMatcher({
remote: expect.objectContaining(tokens),
})
);
});
test('updateAllowedOrigins updates extraOrigins', async () => {
const { updateAllowedOrigins } = await import('@app/store/modules/config.js');
const origins = ['https://test1.com', 'https://test2.com'];
store.dispatch(updateAllowedOrigins(origins));
const state = store.getState().config;
expect(state.api.extraOrigins).toBe(origins.join(', '));
});
test('setUpnpState updates upnp settings', async () => {
const { setUpnpState } = await import('@app/store/modules/config.js');
store.dispatch(setUpnpState({ enabled: 'yes', status: 'active' }));
const state = store.getState().config;
expect(state.remote.upnpEnabled).toBe('yes');
expect(state.connectionStatus.upnpStatus).toBe('active');
});
test('setWanPortToValue updates wanport', async () => {
const { setWanPortToValue } = await import('@app/store/modules/config.js');
store.dispatch(setWanPortToValue(8443));
const state = store.getState().config;
expect(state.remote.wanport).toBe('8443');
});
test('setWanAccess updates wanaccess', async () => {
const { setWanAccess } = await import('@app/store/modules/config.js');
store.dispatch(setWanAccess('yes'));
const state = store.getState().config;
expect(state.remote.wanaccess).toBe('yes');
});
// test('addSsoUser adds user to ssoSubIds', async () => {
// const { addSsoUser } = await import('@app/store/modules/config.js');
// store.dispatch(addSsoUser('user1'));
// store.dispatch(addSsoUser('user2'));
// const state = store.getState().config;
// expect(state.remote.ssoSubIds).toBe('user1,user2');
// });
// test('removeSsoUser removes user from ssoSubIds', async () => {
// const { addSsoUser, removeSsoUser } = await import('@app/store/modules/config.js');
// store.dispatch(addSsoUser('user1'));
// store.dispatch(addSsoUser('user2'));
// store.dispatch(removeSsoUser('user1'));
// const state = store.getState().config;
// expect(state.remote.ssoSubIds).toBe('user2');
// });
// test('removeSsoUser with null clears all ssoSubIds', async () => {
// const { addSsoUser, removeSsoUser } = await import('@app/store/modules/config.js');
// store.dispatch(addSsoUser('user1'));
// store.dispatch(addSsoUser('user2'));
// store.dispatch(removeSsoUser(null));
// const state = store.getState().config;
// expect(state.remote.ssoSubIds).toBe('');
// });
test('setLocalApiKey updates localApiKey', async () => {
const { setLocalApiKey } = await import('@app/store/modules/config.js');
store.dispatch(setLocalApiKey('new-local-api-key'));
const state = store.getState().config;
expect(state.remote.localApiKey).toBe('new-local-api-key');
});
test('setLocalApiKey with null clears localApiKey', async () => {
const { setLocalApiKey } = await import('@app/store/modules/config.js');
store.dispatch(setLocalApiKey(null));
const state = store.getState().config;
expect(state.remote.localApiKey).toBe('');
});
// test('setGraphqlConnectionStatus updates minigraph status', async () => {
// store.dispatch(setGraphqlConnectionStatus({ status: MinigraphStatus.CONNECTED, error: null }));
// const state = store.getState().config;
// expect(state.connectionStatus.minigraph).toBe(MinigraphStatus.CONNECTED);
// });
// test('setupRemoteAccessThunk.fulfilled updates remote access settings', async () => {
// const remoteAccessSettings = {
// accessType: WAN_ACCESS_TYPE.DYNAMIC,
// forwardType: WAN_FORWARD_TYPE.UPNP,
// };
// await store.dispatch(setupRemoteAccessThunk(remoteAccessSettings));
// const state = store.getState().config;
// expect(state.remote).toMatchObject({
// wanaccess: 'no',
// dynamicRemoteAccessType: 'UPNP',
// wanport: '',
// upnpEnabled: 'yes',
// });
// });
});

View File

@@ -24,7 +24,7 @@ test('Before init returns default values for all fields', async () => {
`);
});
test('After init returns values from cfg file for all fields', async () => {
test('After init returns values from cfg file for all fields', { timeout: 30000 }, async () => {
const { loadStateFiles } = await import('@app/store/modules/emhttp.js');
// Load state files into store

View File

@@ -24,7 +24,6 @@ test('Returns paths', async () => {
'myservers-base': '/boot/config/plugins/dynamix.my.servers/',
'myservers-config': expect.stringContaining('api/dev/Unraid.net/myservers.cfg'),
'myservers-config-states': expect.stringContaining('api/dev/states/myservers.cfg'),
'myservers-env': '/boot/config/plugins/dynamix.my.servers/env',
'myservers-keepalive': './dev/Unraid.net/fb_keepalive',
'keyfile-base': expect.stringContaining('api/dev/Unraid.net'),
'machine-id': expect.stringContaining('api/dev/data/machine-id'),

View File

@@ -1,34 +0,0 @@
import { expect, test, vi } from 'vitest';
import { store } from '@app/store/index.js';
import { loadStateFiles } from '@app/store/modules/emhttp.js';
import { loadRegistrationKey } from '@app/store/modules/registration.js';
import { createRegistrationEvent } from '@app/store/sync/registration-sync.js';
vi.mock('@app/core/pubsub', () => ({
pubsub: { publish: vi.fn() },
}));
test('Creates a registration event', async () => {
// Load state files into store
const config = await store.dispatch(loadStateFiles()).unwrap();
await store.dispatch(loadRegistrationKey());
expect(config.var.regFile).toBe('/app/dev/Unraid.net/Pro.key');
const state = store.getState();
const registrationEvent = createRegistrationEvent(state);
expect(registrationEvent).toMatchInlineSnapshot(`
{
"registration": {
"guid": "13FE-4200-C300-58C372A52B19",
"keyFile": {
"contents": "hVs1tLjvC9FiiQsIwIQ7G1KszAcexf0IneThhnmf22SB0dGs5WzRkqMiSMmt2DtR5HOXFUD32YyxuzGeUXmky3zKpSu6xhZNKVg5atGM1OfvkzHBMldI3SeBLuUFSgejLbpNUMdTrbk64JJdbzle4O8wiQgkIpAMIGxeYLwLBD4zHBcfyzq40QnxG--HcX6j25eE0xqa2zWj-j0b0rCAXahJV2a3ySCbPzr1MvfPRTVb0rr7KJ-25R592hYrz4H7Sc1B3p0lr6QUxHE6o7bcYrWKDRtIVoZ8SMPpd1_0gzYIcl5GsDFzFumTXUh8NEnl0Q8hwW1YE-tRc6Y_rrvd7w",
"location": "/app/dev/Unraid.net/Pro.key",
},
"state": "PRO",
"type": "PRO",
},
}
`);
});

View File

@@ -1,20 +0,0 @@
import { type Mapping } from '@runonflux/nat-upnp';
import { expect, test, vi } from 'vitest';
import { getWanPortForUpnp } from '@app/upnp/helpers.js';
test('it successfully gets a wan port given no exclusions', () => {
const port = getWanPortForUpnp(null, 36_000, 38_000);
expect(port).toBeGreaterThan(35_999);
expect(port).toBeLessThan(38_001);
});
test('it fails to get a wan port given exclusions', () => {
const port = getWanPortForUpnp([{ public: { port: 36_000 } }] as Mapping[], 36_000, 36_000);
expect(port).toBeNull();
});
test('it succeeds in getting a wan port given exclusions', () => {
const port = getWanPortForUpnp([{ public: { port: 36_000 } }] as Mapping[], 30_000, 36_000);
expect(port).not.toBeNull();
});

View File

@@ -1,99 +0,0 @@
import { uniq } from 'lodash-es';
import type { RootState } from '@app/store/index.js';
import { logger } from '@app/core/log.js';
import { GRAPHQL_INTROSPECTION } from '@app/environment.js';
import { getServerIps, getUrlForField } from '@app/graphql/resolvers/subscription/network.js';
import { getters, store } from '@app/store/index.js';
import { FileLoadStatus } from '@app/store/types.js';
const getAllowedSocks = (): string[] => [
// Notifier bridge
'/var/run/unraid-notifications.sock',
// Unraid PHP scripts
'/var/run/unraid-php.sock',
// CLI
'/var/run/unraid-cli.sock',
];
const getLocalAccessUrlsForServer = (state: RootState = store.getState()): string[] => {
const { emhttp } = state;
if (emhttp.status !== FileLoadStatus.LOADED) {
return [];
}
const { nginx } = emhttp;
try {
return [
getUrlForField({
url: 'localhost',
port: nginx.httpPort,
}).toString(),
getUrlForField({
url: 'localhost',
portSsl: nginx.httpsPort,
}).toString(),
];
} catch (error: unknown) {
logger.debug('Caught error in getLocalAccessUrlsForServer: \n%o', error);
return [];
}
};
const getRemoteAccessUrlsForAllowedOrigins = (state: RootState = store.getState()): string[] => {
const { urls } = getServerIps(state);
if (urls) {
return urls.reduce<string[]>((acc, curr) => {
if ((curr.ipv4 && curr.ipv6) || curr.ipv4) {
acc.push(curr.ipv4.toString());
} else if (curr.ipv6) {
acc.push(curr.ipv6.toString());
}
return acc;
}, []);
}
return [];
};
export const getExtraOrigins = (): string[] => {
const { extraOrigins } = getters.config().api;
if (extraOrigins) {
return extraOrigins
.replaceAll(' ', '')
.split(',')
.filter((origin) => origin.startsWith('http://') || origin.startsWith('https://'));
}
return [];
};
const getConnectOrigins = (): string[] => {
const connectMain = 'https://connect.myunraid.net';
const connectStaging = 'https://connect-staging.myunraid.net';
const connectDev = 'https://dev-my.myunraid.net:4000';
return [connectMain, connectStaging, connectDev];
};
const getApolloSandbox = (): string[] => {
if (GRAPHQL_INTROSPECTION) {
return ['https://studio.apollographql.com'];
}
return [];
};
export const getAllowedOrigins = (state: RootState = store.getState()): string[] =>
uniq([
...getAllowedSocks(),
...getLocalAccessUrlsForServer(state),
...getRemoteAccessUrlsForAllowedOrigins(state),
...getExtraOrigins(),
...getConnectOrigins(),
...getApolloSandbox(),
]).map((url) => (url.endsWith('/') ? url.slice(0, -1) : url));

View File

@@ -1,40 +0,0 @@
import { isEqual, merge } from 'lodash-es';
import { getAllowedOrigins } from '@app/common/allowed-origins.js';
import { initialState } from '@app/store/modules/config.js';
import {
MyServersConfig,
MyServersConfigMemory,
MyServersConfigMemorySchema,
MyServersConfigSchema,
} from '@app/types/my-servers-config.js';
// Define ConfigType and ConfigObject
export type ConfigType = 'flash' | 'memory';
/**
* Get a writeable configuration based on the mode ('flash' or 'memory').
*/
export const getWriteableConfig = <T extends ConfigType>(
config: T extends 'memory' ? MyServersConfigMemory : MyServersConfig,
mode: T
): T extends 'memory' ? MyServersConfigMemory : MyServersConfig => {
const schema = mode === 'memory' ? MyServersConfigMemorySchema : MyServersConfigSchema;
const defaultConfig = schema.parse(initialState);
// Use a type assertion for the mergedConfig to include `connectionStatus` only if `mode === 'memory`
const mergedConfig = merge<
MyServersConfig,
T extends 'memory' ? MyServersConfigMemory : MyServersConfig
>(defaultConfig, config);
if (mode === 'memory') {
(mergedConfig as MyServersConfigMemory).remote.allowedOrigins = getAllowedOrigins().join(', ');
(mergedConfig as MyServersConfigMemory).connectionStatus = {
...(defaultConfig as MyServersConfigMemory).connectionStatus,
...(config as MyServersConfigMemory).connectionStatus,
};
}
return schema.parse(mergedConfig) as T extends 'memory' ? MyServersConfigMemory : MyServersConfig; // Narrowing ensures correct typing
};

View File

@@ -1,25 +1,40 @@
export const isUnraidApiRunning = async (): Promise<boolean | undefined> => {
const { connect, describe, disconnect } = await import('pm2');
return new Promise((resolve, reject) => {
connect(function (err) {
const { PM2_HOME } = await import('@app/environment.js');
// Set PM2_HOME if not already set
if (!process.env.PM2_HOME) {
process.env.PM2_HOME = PM2_HOME;
}
const pm2Module = await import('pm2');
const pm2 = pm2Module.default || pm2Module;
const pm2Promise = new Promise<boolean>((resolve) => {
pm2.connect(function (err) {
if (err) {
console.error(err);
reject('Could not connect to pm2');
// Don't reject here, resolve with false since we can't connect to PM2
resolve(false);
return;
}
describe('unraid-api', function (err, processDescription) {
console.log(err);
// Now try to describe unraid-api specifically
pm2.describe('unraid-api', function (err, processDescription) {
if (err || processDescription.length === 0) {
console.log(false); // Service not found or error occurred
// Service not found or error occurred
resolve(false);
} else {
const isOnline = processDescription?.[0]?.pm2_env?.status === 'online';
console.log(isOnline); // Output true if online, false otherwise
resolve(isOnline);
}
disconnect();
pm2.disconnect();
});
});
});
const timeoutPromise = new Promise<boolean>((resolve) => {
setTimeout(() => resolve(false), 10000); // 10 second timeout
});
return Promise.race([pm2Promise, timeoutPromise]);
};

View File

@@ -67,6 +67,7 @@ export const getPackageJsonDependencies = (): string[] | undefined => {
export const API_VERSION = process.env.npm_package_version ?? getPackageJson().version;
/** Controls how the app is built/run (i.e. in terms of optimization) */
export const NODE_ENV =
(process.env.NODE_ENV as 'development' | 'test' | 'staging' | 'production') ?? 'production';
export const environment = {
@@ -76,6 +77,7 @@ export const CHOKIDAR_USEPOLLING = process.env.CHOKIDAR_USEPOLLING === 'true';
export const IS_DOCKER = process.env.IS_DOCKER === 'true';
export const DEBUG = process.env.DEBUG === 'true';
export const INTROSPECTION = process.env.INTROSPECTION === 'true';
/** Determines the app-level & business logic environment (i.e. what data & infrastructure is used) */
export const ENVIRONMENT = process.env.ENVIRONMENT
? (process.env.ENVIRONMENT as 'production' | 'staging' | 'development')
: 'production';

View File

@@ -1,77 +0,0 @@
import { ApolloClient, HttpLink, InMemoryCache, split } from '@apollo/client/core/index.js';
import { onError } from '@apollo/client/link/error/index.js';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions/index.js';
import { getMainDefinition } from '@apollo/client/utilities/index.js';
import { fetch } from 'cross-fetch';
import { createClient } from 'graphql-ws';
import WebSocket from 'ws';
import { getInternalApiAddress } from '@app/consts.js';
import { graphqlLogger } from '@app/core/log.js';
import { getters } from '@app/store/index.js';
const getWebsocketWithHeaders = () => {
return class WebsocketWithOriginHeader extends WebSocket {
constructor(address, protocols) {
super(address, protocols, {
headers: {
Origin: '/var/run/unraid-cli.sock',
'Content-Type': 'application/json',
},
});
}
};
};
export const getApiApolloClient = ({ localApiKey }: { localApiKey: string }) => {
const nginxPort = getters?.emhttp()?.nginx?.httpPort ?? 80;
graphqlLogger.debug('Internal GraphQL URL: %s', getInternalApiAddress(true, nginxPort));
const httpLink = new HttpLink({
uri: getInternalApiAddress(true, nginxPort),
fetch,
headers: {
Origin: '/var/run/unraid-cli.sock',
'x-api-key': localApiKey,
'Content-Type': 'application/json',
},
});
// Create the subscription websocket link
const wsLink = new GraphQLWsLink(
createClient({
webSocketImpl: getWebsocketWithHeaders(),
url: getInternalApiAddress(false, nginxPort),
connectionParams: () => {
return { 'x-api-key': localApiKey };
},
})
);
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return definition.kind === 'OperationDefinition' && definition.operation === 'subscription';
},
wsLink,
httpLink
);
const errorLink = onError(({ networkError }) => {
if (networkError) {
graphqlLogger.warn('[GRAPHQL-CLIENT] NETWORK ERROR ENCOUNTERED %o', networkError);
}
});
return new ApolloClient({
defaultOptions: {
query: {
fetchPolicy: 'no-cache',
},
mutate: {
fetchPolicy: 'no-cache',
},
},
cache: new InMemoryCache(),
link: errorLink.concat(splitLink),
});
};

View File

@@ -1,35 +0,0 @@
export const GET_CLOUD_OBJECT = /* GraphQL */ `
query getCloud {
cloud {
error
apiKey {
valid
error
}
minigraphql {
status
timeout
error
}
cloud {
status
error
ip
}
allowedOrigins
}
}
`;
export const GET_SERVERS = /* GraphQL */ `
query getServers {
servers {
name
guid
status
owner {
username
}
}
}
`;

View File

@@ -1,58 +0,0 @@
/* eslint-disable */
import * as types from './graphql.js';
import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
/**
* Map of all GraphQL operations in the project.
*
* This map has several performance disadvantages:
* 1. It is not tree-shakeable, so it will include all operations in the project.
* 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle.
* 3. It does not support dead code elimination, so it will add unused operations.
*
* Therefore it is highly recommended to use the babel or swc plugin for production.
* Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size
*/
type Documents = {
"\n mutation sendRemoteGraphQLResponse($input: RemoteGraphQLServerInput!) {\n remoteGraphQLResponse(input: $input)\n }\n": typeof types.SendRemoteGraphQlResponseDocument,
"\n fragment RemoteGraphQLEventFragment on RemoteGraphQLEvent {\n remoteGraphQLEventData: data {\n type\n body\n sha256\n }\n }\n": typeof types.RemoteGraphQlEventFragmentFragmentDoc,
"\n subscription events {\n events {\n __typename\n ... on ClientConnectedEvent {\n connectedData: data {\n type\n version\n apiKey\n }\n connectedEvent: type\n }\n ... on ClientDisconnectedEvent {\n disconnectedData: data {\n type\n version\n apiKey\n }\n disconnectedEvent: type\n }\n ...RemoteGraphQLEventFragment\n }\n }\n": typeof types.EventsDocument,
};
const documents: Documents = {
"\n mutation sendRemoteGraphQLResponse($input: RemoteGraphQLServerInput!) {\n remoteGraphQLResponse(input: $input)\n }\n": types.SendRemoteGraphQlResponseDocument,
"\n fragment RemoteGraphQLEventFragment on RemoteGraphQLEvent {\n remoteGraphQLEventData: data {\n type\n body\n sha256\n }\n }\n": types.RemoteGraphQlEventFragmentFragmentDoc,
"\n subscription events {\n events {\n __typename\n ... on ClientConnectedEvent {\n connectedData: data {\n type\n version\n apiKey\n }\n connectedEvent: type\n }\n ... on ClientDisconnectedEvent {\n disconnectedData: data {\n type\n version\n apiKey\n }\n disconnectedEvent: type\n }\n ...RemoteGraphQLEventFragment\n }\n }\n": types.EventsDocument,
};
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*
*
* @example
* ```ts
* const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`);
* ```
*
* The query argument is unknown!
* Please regenerate the types.
*/
export function graphql(source: string): unknown;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation sendRemoteGraphQLResponse($input: RemoteGraphQLServerInput!) {\n remoteGraphQLResponse(input: $input)\n }\n"): (typeof documents)["\n mutation sendRemoteGraphQLResponse($input: RemoteGraphQLServerInput!) {\n remoteGraphQLResponse(input: $input)\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment RemoteGraphQLEventFragment on RemoteGraphQLEvent {\n remoteGraphQLEventData: data {\n type\n body\n sha256\n }\n }\n"): (typeof documents)["\n fragment RemoteGraphQLEventFragment on RemoteGraphQLEvent {\n remoteGraphQLEventData: data {\n type\n body\n sha256\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n subscription events {\n events {\n __typename\n ... on ClientConnectedEvent {\n connectedData: data {\n type\n version\n apiKey\n }\n connectedEvent: type\n }\n ... on ClientDisconnectedEvent {\n disconnectedData: data {\n type\n version\n apiKey\n }\n disconnectedEvent: type\n }\n ...RemoteGraphQLEventFragment\n }\n }\n"): (typeof documents)["\n subscription events {\n events {\n __typename\n ... on ClientConnectedEvent {\n connectedData: data {\n type\n version\n apiKey\n }\n connectedEvent: type\n }\n ... on ClientDisconnectedEvent {\n disconnectedData: data {\n type\n version\n apiKey\n }\n disconnectedEvent: type\n }\n ...RemoteGraphQLEventFragment\n }\n }\n"];
export function graphql(source: string) {
return (documents as any)[source] ?? {};
}
export type DocumentType<TDocumentNode extends DocumentNode<any, any>> = TDocumentNode extends DocumentNode< infer TType, any> ? TType : never;

View File

@@ -1,748 +0,0 @@
/* eslint-disable */
import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
export type MakeEmpty<T extends { [key: string]: unknown }, K extends keyof T> = { [_ in K]?: never };
export type Incremental<T> = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: { input: string; output: string; }
String: { input: string; output: string; }
Boolean: { input: boolean; output: boolean; }
Int: { input: number; output: number; }
Float: { input: number; output: number; }
/** A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. */
DateTime: { input: string; output: string; }
/** A field whose value is a IPv4 address: https://en.wikipedia.org/wiki/IPv4. */
IPv4: { input: any; output: any; }
/** A field whose value is a IPv6 address: https://en.wikipedia.org/wiki/IPv6. */
IPv6: { input: any; output: any; }
/** The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */
JSON: { input: Record<string, any>; output: Record<string, any>; }
/** The `Long` scalar type represents 52-bit integers */
Long: { input: number; output: number; }
/** A field whose value is a valid TCP port within the range of 0 to 65535: https://en.wikipedia.org/wiki/Transmission_Control_Protocol#TCP_ports */
Port: { input: number; output: number; }
/** A field whose value conforms to the standard URL format as specified in RFC3986: https://www.ietf.org/rfc/rfc3986.txt. */
URL: { input: URL; output: URL; }
};
export type AccessUrl = {
__typename?: 'AccessUrl';
ipv4?: Maybe<Scalars['URL']['output']>;
ipv6?: Maybe<Scalars['URL']['output']>;
name?: Maybe<Scalars['String']['output']>;
type: UrlType;
};
export type AccessUrlInput = {
ipv4?: InputMaybe<Scalars['URL']['input']>;
ipv6?: InputMaybe<Scalars['URL']['input']>;
name?: InputMaybe<Scalars['String']['input']>;
type: UrlType;
};
export type ArrayCapacity = {
__typename?: 'ArrayCapacity';
bytes?: Maybe<ArrayCapacityBytes>;
};
export type ArrayCapacityBytes = {
__typename?: 'ArrayCapacityBytes';
free?: Maybe<Scalars['Long']['output']>;
total?: Maybe<Scalars['Long']['output']>;
used?: Maybe<Scalars['Long']['output']>;
};
export type ArrayCapacityBytesInput = {
free?: InputMaybe<Scalars['Long']['input']>;
total?: InputMaybe<Scalars['Long']['input']>;
used?: InputMaybe<Scalars['Long']['input']>;
};
export type ArrayCapacityInput = {
bytes?: InputMaybe<ArrayCapacityBytesInput>;
};
export type ClientConnectedEvent = {
__typename?: 'ClientConnectedEvent';
data: ClientConnectionEventData;
type: EventType;
};
export type ClientConnectionEventData = {
__typename?: 'ClientConnectionEventData';
apiKey: Scalars['String']['output'];
type: ClientType;
version: Scalars['String']['output'];
};
export type ClientDisconnectedEvent = {
__typename?: 'ClientDisconnectedEvent';
data: ClientConnectionEventData;
type: EventType;
};
export type ClientPingEvent = {
__typename?: 'ClientPingEvent';
data: PingEventData;
type: EventType;
};
export enum ClientType {
API = 'API',
DASHBOARD = 'DASHBOARD'
}
export type Config = {
__typename?: 'Config';
error?: Maybe<ConfigErrorState>;
valid?: Maybe<Scalars['Boolean']['output']>;
};
export enum ConfigErrorState {
INVALID = 'INVALID',
NO_KEY_SERVER = 'NO_KEY_SERVER',
UNKNOWN_ERROR = 'UNKNOWN_ERROR',
WITHDRAWN = 'WITHDRAWN'
}
export type Dashboard = {
__typename?: 'Dashboard';
apps?: Maybe<DashboardApps>;
array?: Maybe<DashboardArray>;
config?: Maybe<DashboardConfig>;
display?: Maybe<DashboardDisplay>;
id: Scalars['ID']['output'];
lastPublish?: Maybe<Scalars['DateTime']['output']>;
network?: Maybe<Network>;
online?: Maybe<Scalars['Boolean']['output']>;
os?: Maybe<DashboardOs>;
services?: Maybe<Array<Maybe<DashboardService>>>;
twoFactor?: Maybe<DashboardTwoFactor>;
vars?: Maybe<DashboardVars>;
versions?: Maybe<DashboardVersions>;
vms?: Maybe<DashboardVms>;
};
export type DashboardApps = {
__typename?: 'DashboardApps';
installed?: Maybe<Scalars['Int']['output']>;
started?: Maybe<Scalars['Int']['output']>;
};
export type DashboardAppsInput = {
installed: Scalars['Int']['input'];
started: Scalars['Int']['input'];
};
export type DashboardArray = {
__typename?: 'DashboardArray';
/** Current array capacity */
capacity?: Maybe<ArrayCapacity>;
/** Current array state */
state?: Maybe<Scalars['String']['output']>;
};
export type DashboardArrayInput = {
/** Current array capacity */
capacity: ArrayCapacityInput;
/** Current array state */
state: Scalars['String']['input'];
};
export type DashboardCase = {
__typename?: 'DashboardCase';
base64?: Maybe<Scalars['String']['output']>;
error?: Maybe<Scalars['String']['output']>;
icon?: Maybe<Scalars['String']['output']>;
url?: Maybe<Scalars['String']['output']>;
};
export type DashboardCaseInput = {
base64: Scalars['String']['input'];
error?: InputMaybe<Scalars['String']['input']>;
icon: Scalars['String']['input'];
url: Scalars['String']['input'];
};
export type DashboardConfig = {
__typename?: 'DashboardConfig';
error?: Maybe<Scalars['String']['output']>;
valid?: Maybe<Scalars['Boolean']['output']>;
};
export type DashboardConfigInput = {
error?: InputMaybe<Scalars['String']['input']>;
valid: Scalars['Boolean']['input'];
};
export type DashboardDisplay = {
__typename?: 'DashboardDisplay';
case?: Maybe<DashboardCase>;
};
export type DashboardDisplayInput = {
case: DashboardCaseInput;
};
export type DashboardInput = {
apps: DashboardAppsInput;
array: DashboardArrayInput;
config: DashboardConfigInput;
display: DashboardDisplayInput;
os: DashboardOsInput;
services: Array<DashboardServiceInput>;
twoFactor?: InputMaybe<DashboardTwoFactorInput>;
vars: DashboardVarsInput;
versions: DashboardVersionsInput;
vms: DashboardVmsInput;
};
export type DashboardOs = {
__typename?: 'DashboardOs';
hostname?: Maybe<Scalars['String']['output']>;
uptime?: Maybe<Scalars['DateTime']['output']>;
};
export type DashboardOsInput = {
hostname: Scalars['String']['input'];
uptime: Scalars['DateTime']['input'];
};
export type DashboardService = {
__typename?: 'DashboardService';
name?: Maybe<Scalars['String']['output']>;
online?: Maybe<Scalars['Boolean']['output']>;
uptime?: Maybe<DashboardServiceUptime>;
version?: Maybe<Scalars['String']['output']>;
};
export type DashboardServiceInput = {
name: Scalars['String']['input'];
online: Scalars['Boolean']['input'];
uptime?: InputMaybe<DashboardServiceUptimeInput>;
version: Scalars['String']['input'];
};
export type DashboardServiceUptime = {
__typename?: 'DashboardServiceUptime';
timestamp?: Maybe<Scalars['DateTime']['output']>;
};
export type DashboardServiceUptimeInput = {
timestamp: Scalars['DateTime']['input'];
};
export type DashboardTwoFactor = {
__typename?: 'DashboardTwoFactor';
local?: Maybe<DashboardTwoFactorLocal>;
remote?: Maybe<DashboardTwoFactorRemote>;
};
export type DashboardTwoFactorInput = {
local: DashboardTwoFactorLocalInput;
remote: DashboardTwoFactorRemoteInput;
};
export type DashboardTwoFactorLocal = {
__typename?: 'DashboardTwoFactorLocal';
enabled?: Maybe<Scalars['Boolean']['output']>;
};
export type DashboardTwoFactorLocalInput = {
enabled: Scalars['Boolean']['input'];
};
export type DashboardTwoFactorRemote = {
__typename?: 'DashboardTwoFactorRemote';
enabled?: Maybe<Scalars['Boolean']['output']>;
};
export type DashboardTwoFactorRemoteInput = {
enabled: Scalars['Boolean']['input'];
};
export type DashboardVars = {
__typename?: 'DashboardVars';
flashGuid?: Maybe<Scalars['String']['output']>;
regState?: Maybe<Scalars['String']['output']>;
regTy?: Maybe<Scalars['String']['output']>;
serverDescription?: Maybe<Scalars['String']['output']>;
serverName?: Maybe<Scalars['String']['output']>;
};
export type DashboardVarsInput = {
flashGuid: Scalars['String']['input'];
regState: Scalars['String']['input'];
regTy: Scalars['String']['input'];
/** Server description */
serverDescription?: InputMaybe<Scalars['String']['input']>;
/** Name of the server */
serverName?: InputMaybe<Scalars['String']['input']>;
};
export type DashboardVersions = {
__typename?: 'DashboardVersions';
unraid?: Maybe<Scalars['String']['output']>;
};
export type DashboardVersionsInput = {
unraid: Scalars['String']['input'];
};
export type DashboardVms = {
__typename?: 'DashboardVms';
installed?: Maybe<Scalars['Int']['output']>;
started?: Maybe<Scalars['Int']['output']>;
};
export type DashboardVmsInput = {
installed: Scalars['Int']['input'];
started: Scalars['Int']['input'];
};
export type Event = ClientConnectedEvent | ClientDisconnectedEvent | ClientPingEvent | RemoteAccessEvent | RemoteGraphQlEvent | UpdateEvent;
export enum EventType {
CLIENT_CONNECTED_EVENT = 'CLIENT_CONNECTED_EVENT',
CLIENT_DISCONNECTED_EVENT = 'CLIENT_DISCONNECTED_EVENT',
CLIENT_PING_EVENT = 'CLIENT_PING_EVENT',
REMOTE_ACCESS_EVENT = 'REMOTE_ACCESS_EVENT',
REMOTE_GRAPHQL_EVENT = 'REMOTE_GRAPHQL_EVENT',
UPDATE_EVENT = 'UPDATE_EVENT'
}
export type FullServerDetails = {
__typename?: 'FullServerDetails';
apiConnectedCount?: Maybe<Scalars['Int']['output']>;
apiVersion?: Maybe<Scalars['String']['output']>;
connectionTimestamp?: Maybe<Scalars['String']['output']>;
dashboard?: Maybe<Dashboard>;
lastPublish?: Maybe<Scalars['String']['output']>;
network?: Maybe<Network>;
online?: Maybe<Scalars['Boolean']['output']>;
};
export enum Importance {
ALERT = 'ALERT',
INFO = 'INFO',
WARNING = 'WARNING'
}
export type KsServerDetails = {
__typename?: 'KsServerDetails';
accessLabel: Scalars['String']['output'];
accessUrl: Scalars['String']['output'];
apiKey?: Maybe<Scalars['String']['output']>;
description: Scalars['String']['output'];
dnsHash: Scalars['String']['output'];
flashBackupDate?: Maybe<Scalars['Int']['output']>;
flashBackupUrl: Scalars['String']['output'];
flashProduct: Scalars['String']['output'];
flashVendor: Scalars['String']['output'];
guid: Scalars['String']['output'];
ipsId?: Maybe<Scalars['String']['output']>;
keyType?: Maybe<Scalars['String']['output']>;
licenseKey: Scalars['String']['output'];
name: Scalars['String']['output'];
plgVersion?: Maybe<Scalars['String']['output']>;
signedIn: Scalars['Boolean']['output'];
};
export type LegacyService = {
__typename?: 'LegacyService';
name?: Maybe<Scalars['String']['output']>;
online?: Maybe<Scalars['Boolean']['output']>;
uptime?: Maybe<Scalars['Int']['output']>;
version?: Maybe<Scalars['String']['output']>;
};
export type Mutation = {
__typename?: 'Mutation';
remoteGraphQLResponse: Scalars['Boolean']['output'];
remoteMutation: Scalars['String']['output'];
remoteSession?: Maybe<Scalars['Boolean']['output']>;
sendNotification?: Maybe<Notification>;
sendPing?: Maybe<Scalars['Boolean']['output']>;
updateDashboard: Dashboard;
updateNetwork: Network;
};
export type MutationRemoteGraphQlResponseArgs = {
input: RemoteGraphQlServerInput;
};
export type MutationRemoteMutationArgs = {
input: RemoteGraphQlClientInput;
};
export type MutationRemoteSessionArgs = {
remoteAccess: RemoteAccessInput;
};
export type MutationSendNotificationArgs = {
notification: NotificationInput;
};
export type MutationUpdateDashboardArgs = {
data: DashboardInput;
};
export type MutationUpdateNetworkArgs = {
data: NetworkInput;
};
export type Network = {
__typename?: 'Network';
accessUrls?: Maybe<Array<AccessUrl>>;
};
export type NetworkInput = {
accessUrls: Array<AccessUrlInput>;
};
export type Notification = {
__typename?: 'Notification';
description?: Maybe<Scalars['String']['output']>;
importance?: Maybe<Importance>;
link?: Maybe<Scalars['String']['output']>;
status: NotificationStatus;
subject?: Maybe<Scalars['String']['output']>;
title?: Maybe<Scalars['String']['output']>;
};
export type NotificationInput = {
description?: InputMaybe<Scalars['String']['input']>;
importance: Importance;
link?: InputMaybe<Scalars['String']['input']>;
subject?: InputMaybe<Scalars['String']['input']>;
title?: InputMaybe<Scalars['String']['input']>;
};
export enum NotificationStatus {
FAILED_TO_SEND = 'FAILED_TO_SEND',
NOT_FOUND = 'NOT_FOUND',
PENDING = 'PENDING',
SENT = 'SENT'
}
export type PingEvent = {
__typename?: 'PingEvent';
data?: Maybe<Scalars['String']['output']>;
type: EventType;
};
export type PingEventData = {
__typename?: 'PingEventData';
source: PingEventSource;
};
export enum PingEventSource {
API = 'API',
MOTHERSHIP = 'MOTHERSHIP'
}
export type ProfileModel = {
__typename?: 'ProfileModel';
avatar?: Maybe<Scalars['String']['output']>;
cognito_id?: Maybe<Scalars['String']['output']>;
url?: Maybe<Scalars['String']['output']>;
userId?: Maybe<Scalars['ID']['output']>;
username?: Maybe<Scalars['String']['output']>;
};
export type Query = {
__typename?: 'Query';
apiVersion?: Maybe<Scalars['String']['output']>;
dashboard?: Maybe<Dashboard>;
ksServers: Array<KsServerDetails>;
online?: Maybe<Scalars['Boolean']['output']>;
remoteQuery: Scalars['String']['output'];
serverStatus: ServerStatusResponse;
servers: Array<Maybe<Server>>;
status?: Maybe<ServerStatus>;
};
export type QueryDashboardArgs = {
id: Scalars['String']['input'];
};
export type QueryRemoteQueryArgs = {
input: RemoteGraphQlClientInput;
};
export type QueryServerStatusArgs = {
apiKey: Scalars['String']['input'];
};
export enum RegistrationState {
/** Basic */
BASIC = 'BASIC',
/** BLACKLISTED */
EBLACKLISTED = 'EBLACKLISTED',
/** BLACKLISTED */
EBLACKLISTED1 = 'EBLACKLISTED1',
/** BLACKLISTED */
EBLACKLISTED2 = 'EBLACKLISTED2',
/** Trial Expired */
EEXPIRED = 'EEXPIRED',
/** GUID Error */
EGUID = 'EGUID',
/** Multiple License Keys Present */
EGUID1 = 'EGUID1',
/** Trial Requires Internet Connection */
ENOCONN = 'ENOCONN',
/** No Flash */
ENOFLASH = 'ENOFLASH',
ENOFLASH1 = 'ENOFLASH1',
ENOFLASH2 = 'ENOFLASH2',
ENOFLASH3 = 'ENOFLASH3',
ENOFLASH4 = 'ENOFLASH4',
ENOFLASH5 = 'ENOFLASH5',
ENOFLASH6 = 'ENOFLASH6',
ENOFLASH7 = 'ENOFLASH7',
/** No Keyfile */
ENOKEYFILE = 'ENOKEYFILE',
/** No Keyfile */
ENOKEYFILE1 = 'ENOKEYFILE1',
/** Missing key file */
ENOKEYFILE2 = 'ENOKEYFILE2',
/** Invalid installation */
ETRIAL = 'ETRIAL',
/** Plus */
PLUS = 'PLUS',
/** Pro */
PRO = 'PRO',
/** Trial */
TRIAL = 'TRIAL'
}
export type RemoteAccessEvent = {
__typename?: 'RemoteAccessEvent';
data: RemoteAccessEventData;
type: EventType;
};
/** Defines whether remote access event is the initiation (from connect) or the response (from the server) */
export enum RemoteAccessEventActionType {
ACK = 'ACK',
END = 'END',
INIT = 'INIT',
PING = 'PING'
}
export type RemoteAccessEventData = {
__typename?: 'RemoteAccessEventData';
apiKey: Scalars['String']['output'];
type: RemoteAccessEventActionType;
url?: Maybe<AccessUrl>;
};
export type RemoteAccessInput = {
apiKey: Scalars['String']['input'];
type: RemoteAccessEventActionType;
url?: InputMaybe<AccessUrlInput>;
};
export type RemoteGraphQlClientInput = {
apiKey: Scalars['String']['input'];
body: Scalars['String']['input'];
/** Time in milliseconds to wait for a response from the remote server (defaults to 15000) */
timeout?: InputMaybe<Scalars['Int']['input']>;
/** How long mothership should cache the result of this query in seconds, only valid on queries */
ttl?: InputMaybe<Scalars['Int']['input']>;
};
export type RemoteGraphQlEvent = {
__typename?: 'RemoteGraphQLEvent';
data: RemoteGraphQlEventData;
type: EventType;
};
export type RemoteGraphQlEventData = {
__typename?: 'RemoteGraphQLEventData';
/** Contains mutation / subscription / query data in the form of body: JSON, variables: JSON */
body: Scalars['String']['output'];
/** sha256 hash of the body */
sha256: Scalars['String']['output'];
type: RemoteGraphQlEventType;
};
export enum RemoteGraphQlEventType {
REMOTE_MUTATION_EVENT = 'REMOTE_MUTATION_EVENT',
REMOTE_QUERY_EVENT = 'REMOTE_QUERY_EVENT',
REMOTE_SUBSCRIPTION_EVENT = 'REMOTE_SUBSCRIPTION_EVENT',
REMOTE_SUBSCRIPTION_EVENT_PING = 'REMOTE_SUBSCRIPTION_EVENT_PING'
}
export type RemoteGraphQlServerInput = {
/** Body - contains an object containing data: (GQL response data) or errors: (GQL Errors) */
body: Scalars['String']['input'];
/** sha256 hash of the body */
sha256: Scalars['String']['input'];
type: RemoteGraphQlEventType;
};
export type Server = {
__typename?: 'Server';
apikey?: Maybe<Scalars['String']['output']>;
guid?: Maybe<Scalars['String']['output']>;
lanip?: Maybe<Scalars['String']['output']>;
localurl?: Maybe<Scalars['String']['output']>;
name?: Maybe<Scalars['String']['output']>;
owner?: Maybe<ProfileModel>;
remoteurl?: Maybe<Scalars['String']['output']>;
status?: Maybe<ServerStatus>;
wanip?: Maybe<Scalars['String']['output']>;
};
/** Defines server fields that have a TTL on them, for example last ping */
export type ServerFieldsWithTtl = {
__typename?: 'ServerFieldsWithTtl';
lastPing?: Maybe<Scalars['String']['output']>;
};
export type ServerModel = {
apikey: Scalars['String']['output'];
guid: Scalars['String']['output'];
lanip: Scalars['String']['output'];
localurl: Scalars['String']['output'];
name: Scalars['String']['output'];
remoteurl: Scalars['String']['output'];
wanip: Scalars['String']['output'];
};
export enum ServerStatus {
NEVER_CONNECTED = 'never_connected',
OFFLINE = 'offline',
ONLINE = 'online'
}
export type ServerStatusResponse = {
__typename?: 'ServerStatusResponse';
id: Scalars['ID']['output'];
lastPublish?: Maybe<Scalars['String']['output']>;
online: Scalars['Boolean']['output'];
};
export type Service = {
__typename?: 'Service';
name?: Maybe<Scalars['String']['output']>;
online?: Maybe<Scalars['Boolean']['output']>;
uptime?: Maybe<Uptime>;
version?: Maybe<Scalars['String']['output']>;
};
export type Subscription = {
__typename?: 'Subscription';
events?: Maybe<Array<Event>>;
remoteSubscription: Scalars['String']['output'];
servers: Array<Server>;
};
export type SubscriptionRemoteSubscriptionArgs = {
input: RemoteGraphQlClientInput;
};
export type TwoFactorLocal = {
__typename?: 'TwoFactorLocal';
enabled?: Maybe<Scalars['Boolean']['output']>;
};
export type TwoFactorRemote = {
__typename?: 'TwoFactorRemote';
enabled?: Maybe<Scalars['Boolean']['output']>;
};
export type TwoFactorWithToken = {
__typename?: 'TwoFactorWithToken';
local?: Maybe<TwoFactorLocal>;
remote?: Maybe<TwoFactorRemote>;
token?: Maybe<Scalars['String']['output']>;
};
export type TwoFactorWithoutToken = {
__typename?: 'TwoFactorWithoutToken';
local?: Maybe<TwoFactorLocal>;
remote?: Maybe<TwoFactorRemote>;
};
export enum UrlType {
DEFAULT = 'DEFAULT',
LAN = 'LAN',
MDNS = 'MDNS',
WAN = 'WAN',
WIREGUARD = 'WIREGUARD'
}
export type UpdateEvent = {
__typename?: 'UpdateEvent';
data: UpdateEventData;
type: EventType;
};
export type UpdateEventData = {
__typename?: 'UpdateEventData';
apiKey: Scalars['String']['output'];
type: UpdateType;
};
export enum UpdateType {
DASHBOARD = 'DASHBOARD',
NETWORK = 'NETWORK'
}
export type Uptime = {
__typename?: 'Uptime';
timestamp?: Maybe<Scalars['String']['output']>;
};
export type UserProfileModelWithServers = {
__typename?: 'UserProfileModelWithServers';
profile: ProfileModel;
servers: Array<Server>;
};
export type Vars = {
__typename?: 'Vars';
expireTime?: Maybe<Scalars['DateTime']['output']>;
flashGuid?: Maybe<Scalars['String']['output']>;
regState?: Maybe<RegistrationState>;
regTm2?: Maybe<Scalars['String']['output']>;
regTy?: Maybe<Scalars['String']['output']>;
};
export type SendRemoteGraphQlResponseMutationVariables = Exact<{
input: RemoteGraphQlServerInput;
}>;
export type SendRemoteGraphQlResponseMutation = { __typename?: 'Mutation', remoteGraphQLResponse: boolean };
export type RemoteGraphQlEventFragmentFragment = { __typename?: 'RemoteGraphQLEvent', remoteGraphQLEventData: { __typename?: 'RemoteGraphQLEventData', type: RemoteGraphQlEventType, body: string, sha256: string } } & { ' $fragmentName'?: 'RemoteGraphQlEventFragmentFragment' };
export type EventsSubscriptionVariables = Exact<{ [key: string]: never; }>;
export type EventsSubscription = { __typename?: 'Subscription', events?: Array<{ __typename: 'ClientConnectedEvent', connectedEvent: EventType, connectedData: { __typename?: 'ClientConnectionEventData', type: ClientType, version: string, apiKey: string } } | { __typename: 'ClientDisconnectedEvent', disconnectedEvent: EventType, disconnectedData: { __typename?: 'ClientConnectionEventData', type: ClientType, version: string, apiKey: string } } | { __typename: 'ClientPingEvent' } | { __typename: 'RemoteAccessEvent' } | (
{ __typename: 'RemoteGraphQLEvent' }
& { ' $fragmentRefs'?: { 'RemoteGraphQlEventFragmentFragment': RemoteGraphQlEventFragmentFragment } }
) | { __typename: 'UpdateEvent' }> | null };
export const RemoteGraphQlEventFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteGraphQLEventFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteGraphQLEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"remoteGraphQLEventData"},"name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"body"}},{"kind":"Field","name":{"kind":"Name","value":"sha256"}}]}}]}}]} as unknown as DocumentNode<RemoteGraphQlEventFragmentFragment, unknown>;
export const SendRemoteGraphQlResponseDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"sendRemoteGraphQLResponse"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteGraphQLServerInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"remoteGraphQLResponse"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<SendRemoteGraphQlResponseMutation, SendRemoteGraphQlResponseMutationVariables>;
export const EventsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"events"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"events"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ClientConnectedEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"connectedData"},"name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"}}]}},{"kind":"Field","alias":{"kind":"Name","value":"connectedEvent"},"name":{"kind":"Name","value":"type"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ClientDisconnectedEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"disconnectedData"},"name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"}}]}},{"kind":"Field","alias":{"kind":"Name","value":"disconnectedEvent"},"name":{"kind":"Name","value":"type"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"RemoteGraphQLEventFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteGraphQLEventFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteGraphQLEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"remoteGraphQLEventData"},"name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"body"}},{"kind":"Field","name":{"kind":"Name","value":"sha256"}}]}}]}}]} as unknown as DocumentNode<EventsSubscription, EventsSubscriptionVariables>;

View File

@@ -1,2 +0,0 @@
export * from "./fragment-masking.js";
export * from "./gql.js";

View File

@@ -1,216 +0,0 @@
/* eslint-disable */
import { z } from 'zod'
import { AccessUrlInput, ArrayCapacityBytesInput, ArrayCapacityInput, ClientType, ConfigErrorState, DashboardAppsInput, DashboardArrayInput, DashboardCaseInput, DashboardConfigInput, DashboardDisplayInput, DashboardInput, DashboardOsInput, DashboardServiceInput, DashboardServiceUptimeInput, DashboardTwoFactorInput, DashboardTwoFactorLocalInput, DashboardTwoFactorRemoteInput, DashboardVarsInput, DashboardVersionsInput, DashboardVmsInput, EventType, Importance, NetworkInput, NotificationInput, NotificationStatus, PingEventSource, RegistrationState, RemoteAccessEventActionType, RemoteAccessInput, RemoteGraphQlClientInput, RemoteGraphQlEventType, RemoteGraphQlServerInput, ServerStatus, UrlType, UpdateType } from '@app/graphql/generated/client/graphql.js'
type Properties<T> = Required<{
[K in keyof T]: z.ZodType<T[K], any, T[K]>;
}>;
type definedNonNullAny = {};
export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== undefined && v !== null;
export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v));
export const ClientTypeSchema = z.nativeEnum(ClientType);
export const ConfigErrorStateSchema = z.nativeEnum(ConfigErrorState);
export const EventTypeSchema = z.nativeEnum(EventType);
export const ImportanceSchema = z.nativeEnum(Importance);
export const NotificationStatusSchema = z.nativeEnum(NotificationStatus);
export const PingEventSourceSchema = z.nativeEnum(PingEventSource);
export const RegistrationStateSchema = z.nativeEnum(RegistrationState);
export const RemoteAccessEventActionTypeSchema = z.nativeEnum(RemoteAccessEventActionType);
export const RemoteGraphQlEventTypeSchema = z.nativeEnum(RemoteGraphQlEventType);
export const ServerStatusSchema = z.nativeEnum(ServerStatus);
export const UrlTypeSchema = z.nativeEnum(UrlType);
export const UpdateTypeSchema = z.nativeEnum(UpdateType);
export function AccessUrlInputSchema(): z.ZodObject<Properties<AccessUrlInput>> {
return z.object({
ipv4: z.instanceof(URL).nullish(),
ipv6: z.instanceof(URL).nullish(),
name: z.string().nullish(),
type: UrlTypeSchema
})
}
export function ArrayCapacityBytesInputSchema(): z.ZodObject<Properties<ArrayCapacityBytesInput>> {
return z.object({
free: z.number().nullish(),
total: z.number().nullish(),
used: z.number().nullish()
})
}
export function ArrayCapacityInputSchema(): z.ZodObject<Properties<ArrayCapacityInput>> {
return z.object({
bytes: z.lazy(() => ArrayCapacityBytesInputSchema().nullish())
})
}
export function DashboardAppsInputSchema(): z.ZodObject<Properties<DashboardAppsInput>> {
return z.object({
installed: z.number(),
started: z.number()
})
}
export function DashboardArrayInputSchema(): z.ZodObject<Properties<DashboardArrayInput>> {
return z.object({
capacity: z.lazy(() => ArrayCapacityInputSchema()),
state: z.string()
})
}
export function DashboardCaseInputSchema(): z.ZodObject<Properties<DashboardCaseInput>> {
return z.object({
base64: z.string(),
error: z.string().nullish(),
icon: z.string(),
url: z.string()
})
}
export function DashboardConfigInputSchema(): z.ZodObject<Properties<DashboardConfigInput>> {
return z.object({
error: z.string().nullish(),
valid: z.boolean()
})
}
export function DashboardDisplayInputSchema(): z.ZodObject<Properties<DashboardDisplayInput>> {
return z.object({
case: z.lazy(() => DashboardCaseInputSchema())
})
}
export function DashboardInputSchema(): z.ZodObject<Properties<DashboardInput>> {
return z.object({
apps: z.lazy(() => DashboardAppsInputSchema()),
array: z.lazy(() => DashboardArrayInputSchema()),
config: z.lazy(() => DashboardConfigInputSchema()),
display: z.lazy(() => DashboardDisplayInputSchema()),
os: z.lazy(() => DashboardOsInputSchema()),
services: z.array(z.lazy(() => DashboardServiceInputSchema())),
twoFactor: z.lazy(() => DashboardTwoFactorInputSchema().nullish()),
vars: z.lazy(() => DashboardVarsInputSchema()),
versions: z.lazy(() => DashboardVersionsInputSchema()),
vms: z.lazy(() => DashboardVmsInputSchema())
})
}
export function DashboardOsInputSchema(): z.ZodObject<Properties<DashboardOsInput>> {
return z.object({
hostname: z.string(),
uptime: z.string()
})
}
export function DashboardServiceInputSchema(): z.ZodObject<Properties<DashboardServiceInput>> {
return z.object({
name: z.string(),
online: z.boolean(),
uptime: z.lazy(() => DashboardServiceUptimeInputSchema().nullish()),
version: z.string()
})
}
export function DashboardServiceUptimeInputSchema(): z.ZodObject<Properties<DashboardServiceUptimeInput>> {
return z.object({
timestamp: z.string()
})
}
export function DashboardTwoFactorInputSchema(): z.ZodObject<Properties<DashboardTwoFactorInput>> {
return z.object({
local: z.lazy(() => DashboardTwoFactorLocalInputSchema()),
remote: z.lazy(() => DashboardTwoFactorRemoteInputSchema())
})
}
export function DashboardTwoFactorLocalInputSchema(): z.ZodObject<Properties<DashboardTwoFactorLocalInput>> {
return z.object({
enabled: z.boolean()
})
}
export function DashboardTwoFactorRemoteInputSchema(): z.ZodObject<Properties<DashboardTwoFactorRemoteInput>> {
return z.object({
enabled: z.boolean()
})
}
export function DashboardVarsInputSchema(): z.ZodObject<Properties<DashboardVarsInput>> {
return z.object({
flashGuid: z.string(),
regState: z.string(),
regTy: z.string(),
serverDescription: z.string().nullish(),
serverName: z.string().nullish()
})
}
export function DashboardVersionsInputSchema(): z.ZodObject<Properties<DashboardVersionsInput>> {
return z.object({
unraid: z.string()
})
}
export function DashboardVmsInputSchema(): z.ZodObject<Properties<DashboardVmsInput>> {
return z.object({
installed: z.number(),
started: z.number()
})
}
export function NetworkInputSchema(): z.ZodObject<Properties<NetworkInput>> {
return z.object({
accessUrls: z.array(z.lazy(() => AccessUrlInputSchema()))
})
}
export function NotificationInputSchema(): z.ZodObject<Properties<NotificationInput>> {
return z.object({
description: z.string().nullish(),
importance: ImportanceSchema,
link: z.string().nullish(),
subject: z.string().nullish(),
title: z.string().nullish()
})
}
export function RemoteAccessInputSchema(): z.ZodObject<Properties<RemoteAccessInput>> {
return z.object({
apiKey: z.string(),
type: RemoteAccessEventActionTypeSchema,
url: z.lazy(() => AccessUrlInputSchema().nullish())
})
}
export function RemoteGraphQlClientInputSchema(): z.ZodObject<Properties<RemoteGraphQlClientInput>> {
return z.object({
apiKey: z.string(),
body: z.string(),
timeout: z.number().nullish(),
ttl: z.number().nullish()
})
}
export function RemoteGraphQlServerInputSchema(): z.ZodObject<Properties<RemoteGraphQlServerInput>> {
return z.object({
body: z.string(),
sha256: z.string(),
type: RemoteGraphQlEventTypeSchema
})
}

View File

@@ -1,10 +0,0 @@
import { FatalAppError } from '@app/core/errors/fatal-error.js';
import { modules } from '@app/core/index.js';
export const getCoreModule = (moduleName: string) => {
if (!Object.keys(modules).includes(moduleName)) {
throw new FatalAppError(`"${moduleName}" is not a valid core module.`);
}
return modules[moduleName];
};

View File

@@ -1,7 +0,0 @@
import { graphql } from '@app/graphql/generated/client/gql.js';
export const SEND_REMOTE_QUERY_RESPONSE = graphql(/* GraphQL */ `
mutation sendRemoteGraphQLResponse($input: RemoteGraphQLServerInput!) {
remoteGraphQLResponse(input: $input)
}
`);

View File

@@ -1,36 +0,0 @@
import { graphql } from '@app/graphql/generated/client/gql.js';
export const RemoteGraphQL_Fragment = graphql(/* GraphQL */ `
fragment RemoteGraphQLEventFragment on RemoteGraphQLEvent {
remoteGraphQLEventData: data {
type
body
sha256
}
}
`);
export const EVENTS_SUBSCRIPTION = graphql(/* GraphQL */ `
subscription events {
events {
__typename
... on ClientConnectedEvent {
connectedData: data {
type
version
apiKey
}
connectedEvent: type
}
... on ClientDisconnectedEvent {
disconnectedData: data {
type
version
apiKey
}
disconnectedEvent: type
}
...RemoteGraphQLEventFragment
}
}
`);

View File

@@ -1,234 +0,0 @@
import { AccessUrl, URL_TYPE } from '@unraid/shared/network.model.js';
import type { RootState } from '@app/store/index.js';
import { logger } from '@app/core/log.js';
import { type Nginx } from '@app/core/types/states/nginx.js';
import { store } from '@app/store/index.js';
interface UrlForFieldInput {
url: string;
port?: number;
portSsl?: number;
}
interface UrlForFieldInputSecure extends UrlForFieldInput {
url: string;
portSsl: number;
}
interface UrlForFieldInputInsecure extends UrlForFieldInput {
url: string;
port: number;
}
export const getUrlForField = ({
url,
port,
portSsl,
}: UrlForFieldInputInsecure | UrlForFieldInputSecure) => {
let portToUse = '';
let httpMode = 'https://';
if (!url || url === '') {
throw new Error('No URL Provided');
}
if (port) {
portToUse = port === 80 ? '' : `:${port}`;
httpMode = 'http://';
} else if (portSsl) {
portToUse = portSsl === 443 ? '' : `:${portSsl}`;
httpMode = 'https://';
} else {
throw new Error(`No ports specified for URL: ${url}`);
}
const urlString = `${httpMode}${url}${portToUse}`;
try {
return new URL(urlString);
} catch (error: unknown) {
throw new Error(`Failed to parse URL: ${urlString}`);
}
};
const fieldIsFqdn = (field: keyof Nginx) => field?.toLowerCase().includes('fqdn');
export type NginxUrlFields = Extract<
keyof Nginx,
'lanIp' | 'lanIp6' | 'lanName' | 'lanMdns' | 'lanFqdn' | 'wanFqdn' | 'wanFqdn6'
>;
/**
*
* @param nginx Nginx Config File
* @param field The field to build the URL from
* @returns a URL, created from the combination of inputs
* @throws Error when the URL cannot be created or the URL is invalid
*/
export const getUrlForServer = ({ nginx, field }: { nginx: Nginx; field: NginxUrlFields }): URL => {
if (nginx[field]) {
if (fieldIsFqdn(field)) {
return getUrlForField({
url: nginx[field],
portSsl: nginx.httpsPort,
});
}
if (!nginx.sslEnabled) {
// Use SSL = no
return getUrlForField({ url: nginx[field], port: nginx.httpPort });
}
if (nginx.sslMode === 'yes') {
return getUrlForField({
url: nginx[field],
portSsl: nginx.httpsPort,
});
}
if (nginx.sslMode === 'auto') {
throw new Error(`Cannot get IP Based URL for field: "${field}" SSL mode auto`);
}
}
throw new Error(
`IP URL Resolver: Could not resolve any access URL for field: "${field}", is FQDN?: ${fieldIsFqdn(
field
)}`
);
};
const getUrlTypeFromFqdn = (fqdnType: string): URL_TYPE => {
switch (fqdnType) {
case 'LAN':
return URL_TYPE.LAN;
case 'WAN':
return URL_TYPE.WAN;
case 'WG':
return URL_TYPE.WIREGUARD;
default:
// HACK: This should be added as a new type (e.g. OTHER or CUSTOM)
return URL_TYPE.WIREGUARD;
}
};
export const getServerIps = (
state: RootState = store.getState()
): { urls: AccessUrl[]; errors: Error[] } => {
const { nginx } = state.emhttp;
const {
remote: { wanport },
} = state.config;
if (!nginx || Object.keys(nginx).length === 0) {
return { urls: [], errors: [new Error('Nginx Not Loaded')] };
}
const errors: Error[] = [];
const urls: AccessUrl[] = [];
try {
// Default URL
const defaultUrl = new URL(nginx.defaultUrl);
urls.push({
name: 'Default',
type: URL_TYPE.DEFAULT,
ipv4: defaultUrl,
ipv6: defaultUrl,
});
} catch (error: unknown) {
if (error instanceof Error) {
errors.push(error);
} else {
logger.warn('Uncaught error in network resolver', error);
}
}
try {
// Lan IP URL
const lanIp4Url = getUrlForServer({ nginx, field: 'lanIp' });
urls.push({
name: 'LAN IPv4',
type: URL_TYPE.LAN,
ipv4: lanIp4Url,
});
} catch (error: unknown) {
if (error instanceof Error) {
errors.push(error);
} else {
logger.warn('Uncaught error in network resolver', error);
}
}
try {
// Lan IP6 URL
const lanIp6Url = getUrlForServer({ nginx, field: 'lanIp6' });
urls.push({
name: 'LAN IPv6',
type: URL_TYPE.LAN,
ipv4: lanIp6Url,
});
} catch (error: unknown) {
if (error instanceof Error) {
errors.push(error);
} else {
logger.warn('Uncaught error in network resolver', error);
}
}
try {
// Lan Name URL
const lanNameUrl = getUrlForServer({ nginx, field: 'lanName' });
urls.push({
name: 'LAN Name',
type: URL_TYPE.MDNS,
ipv4: lanNameUrl,
});
} catch (error: unknown) {
if (error instanceof Error) {
errors.push(error);
} else {
logger.warn('Uncaught error in network resolver', error);
}
}
try {
// Lan MDNS URL
const lanMdnsUrl = getUrlForServer({ nginx, field: 'lanMdns' });
urls.push({
name: 'LAN MDNS',
type: URL_TYPE.MDNS,
ipv4: lanMdnsUrl,
});
} catch (error: unknown) {
if (error instanceof Error) {
errors.push(error);
} else {
logger.warn('Uncaught error in network resolver', error);
}
}
// Now Process the FQDN Urls
nginx.fqdnUrls.forEach((fqdnUrl) => {
try {
const urlType = getUrlTypeFromFqdn(fqdnUrl.interface);
const fqdnUrlToUse = getUrlForField({
url: fqdnUrl.fqdn,
portSsl: urlType === URL_TYPE.WAN ? Number(wanport) : nginx.httpsPort,
});
urls.push({
name: `FQDN ${fqdnUrl.interface}${fqdnUrl.id !== null ? ` ${fqdnUrl.id}` : ''}`,
type: getUrlTypeFromFqdn(fqdnUrl.interface),
ipv4: fqdnUrlToUse,
});
} catch (error: unknown) {
if (error instanceof Error) {
errors.push(error);
} else {
logger.warn('Uncaught error in network resolver', error);
}
}
});
return { urls, errors };
};

View File

@@ -1,28 +0,0 @@
import { mergeTypeDefs } from '@graphql-tools/merge';
import { logger } from '@app/core/log.js';
export const loadTypeDefs = async (additionalTypeDefs: string[] = []) => {
// TypeScript now knows this returns Record<string, () => Promise<string>>
const typeModules = import.meta.glob('./types/**/*.graphql', { query: '?raw', import: 'default' });
try {
const files = await Promise.all(
Object.values(typeModules).map(async (importFn) => {
const content = await importFn();
if (typeof content !== 'string') {
throw new Error('Invalid GraphQL type definition format');
}
return content;
})
);
if (!files.length) {
throw new Error('No GraphQL type definitions found');
}
files.push(...additionalTypeDefs);
return mergeTypeDefs(files);
} catch (error) {
logger.error('Failed to load GraphQL type definitions:', error);
throw error;
}
};

View File

@@ -1,111 +0,0 @@
import { AppError } from '@app/core/errors/app-error.js';
import { graphqlLogger } from '@app/core/log.js';
import { pubsub } from '@app/core/pubsub.js';
import { type User } from '@app/core/types/states/user.js';
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission.js';
import { store } from '@app/store/index.js';
import { MinigraphStatus } from '@app/unraid-api/graph/resolvers/cloud/cloud.model.js';
import { Server, ServerStatus } from '@app/unraid-api/graph/resolvers/servers/server.model.js';
export interface Context {
user?: User;
websocketId: string;
}
type Subscription = {
total: number;
channels: string[];
};
const subscriptions: Record<string, Subscription> = {};
/**
* Return current ws connection count.
*/
export const getWsConnectionCount = () =>
Object.values(subscriptions).filter((subscription) => subscription.total >= 1).length;
/**
* Return current ws connection count in channel.
*/
export const getWsConnectionCountInChannel = (channel: string) =>
Object.values(subscriptions).filter((subscription) => subscription.channels.includes(channel))
.length;
export const hasSubscribedToChannel = (id: string, channel: string) => {
graphqlLogger.debug('Subscribing to %s', channel);
// Setup initial object
if (subscriptions[id] === undefined) {
subscriptions[id] = {
total: 1,
channels: [channel],
};
return;
}
subscriptions[id].total++;
subscriptions[id].channels.push(channel);
};
/**
* Create a pubsub subscription.
* @param channel The pubsub channel to subscribe to.
* @param resource The access-control permission resource to check against.
*/
export const createSubscription = (channel: string, resource?: string) => ({
subscribe(_: unknown, __: unknown, context: Context) {
if (!context.user) {
throw new AppError('<ws> No user found in context.', 500);
}
// Check the user has permission to subscribe to this endpoint
ensurePermission(context.user, {
resource: resource ?? channel,
action: 'read',
possession: 'any',
});
hasSubscribedToChannel(context.websocketId, channel);
return pubsub.asyncIterableIterator(channel);
},
});
export const getLocalServer = (getState = store.getState): Array<Server> => {
const { emhttp, config, minigraph } = getState();
const guid = emhttp.var.regGuid;
const { name } = emhttp.var;
const wanip = '';
const lanip: string = emhttp.networks[0].ipaddr[0];
const port = emhttp.var?.port;
const localurl = `http://${lanip}:${port}`;
const remoteurl = '';
return [
{
id: 'local',
owner: {
id: 'local',
username: config.remote.username ?? 'root',
url: '',
avatar: '',
},
guid,
apikey: config.remote.apikey ?? '',
name: name ?? 'Local Server',
status:
minigraph.status === MinigraphStatus.CONNECTED
? ServerStatus.ONLINE
: ServerStatus.OFFLINE,
wanip,
lanip,
localurl,
remoteurl,
},
];
};
export const getServers = (getState = store.getState): Server[] => {
// Check if we have the servers already cached, if so return them
return getLocalServer(getState) ?? [];
};

View File

@@ -22,10 +22,8 @@ import { loadDynamixConfigFile } from '@app/store/actions/load-dynamix-config-fi
import { shutdownApiEvent } from '@app/store/actions/shutdown-api-event.js';
import { store } from '@app/store/index.js';
import { startMiddlewareListeners } from '@app/store/listeners/listener-middleware.js';
import { loadConfigFile } from '@app/store/modules/config.js';
import { loadStateFiles } from '@app/store/modules/emhttp.js';
import { loadRegistrationKey } from '@app/store/modules/registration.js';
import { startStoreSync } from '@app/store/store-sync.js';
import { setupDynamixConfigWatch } from '@app/store/watch/dynamix-config-watch.js';
import { setupRegistrationKeyWatch } from '@app/store/watch/registration-watch.js';
import { StateManager } from '@app/store/watch/state-watch.js';
@@ -71,13 +69,6 @@ export const viteNodeApp = async () => {
cacheable.install(http.globalAgent);
cacheable.install(https.globalAgent);
// Start file <-> store sync
// Must occur before config is loaded to ensure that the handler can fix broken configs
await startStoreSync();
// Load my servers config file into store
await store.dispatch(loadConfigFile());
// Load emhttp state into store
await store.dispatch(loadStateFiles());

View File

@@ -1,12 +0,0 @@
/**
* Check is the API Key is the correct length (64 characters)
* @param apiKey API Key to validate length
* @returns Boolean
*/
export const isApiKeyCorrectLength = (apiKey: string) => {
if (apiKey.length !== 64) {
return false;
}
return true;
};

View File

@@ -1,19 +0,0 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { remoteAccessLogger } from '@app/core/log.js';
import { NginxManager } from '@app/core/modules/services/nginx.js';
import { UpdateDNSManager } from '@app/core/modules/services/update-dns.js';
import { type AppDispatch, type RootState } from '@app/store/index.js';
export const reloadNginxAndUpdateDNS = createAsyncThunk<
void,
void,
{ state: RootState; dispatch: AppDispatch }
>('config/reloadNginxAndUpdateDNS', async () => {
remoteAccessLogger.debug('Reloading Nginx and Updating DNS');
const manager = new NginxManager();
const updateDns = new UpdateDNSManager();
await manager.reloadNginx();
await updateDns.updateDNS();
remoteAccessLogger.debug('Finished Reloading Nginx and Updating DNS');
});

View File

@@ -1,8 +0,0 @@
import { createAction } from '@reduxjs/toolkit';
import { MinigraphStatus } from '@app/unraid-api/graph/resolvers/cloud/cloud.model.js';
export const setGraphqlConnectionStatus = createAction<{
status: MinigraphStatus;
error: string | null;
}>('graphql/status');

View File

@@ -1,16 +0,0 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { reloadNginxAndUpdateDNS } from '@app/store/actions/reload-nginx-and-update-dns.js';
import { type AppDispatch, type RootState } from '@app/store/index.js';
import { setWanAccess } from '@app/store/modules/config.js';
type EnableWanAccessArgs = Parameters<typeof setWanAccess>[0];
export const setWanAccessAndReloadNginx = createAsyncThunk<
void,
EnableWanAccessArgs,
{ state: RootState; dispatch: AppDispatch }
>('config/setWanAccessAndReloadNginx', async (payload, { dispatch }) => {
dispatch(setWanAccess(payload));
await dispatch(reloadNginxAndUpdateDNS());
});

View File

@@ -1,13 +1,9 @@
import { logDestination, logger } from '@app/core/log.js';
import { stopListeners } from '@app/store/listeners/stop-listeners.js';
import { writeConfigSync } from '@app/store/sync/config-disk-sync.js';
export const shutdownApiEvent = () => {
logger.debug('Running shutdown');
stopListeners();
logger.debug('Writing final configs');
writeConfigSync('flash');
writeConfigSync('memory');
logger.debug('Shutting down log destination');
logDestination.flushSync();
logDestination.destroy();

View File

@@ -16,11 +16,8 @@ export type AppDispatch = typeof store.dispatch;
export type ApiStore = typeof store;
export const getters = {
config: () => store.getState().config,
dynamix: () => store.getState().dynamix,
emhttp: () => store.getState().emhttp,
minigraph: () => store.getState().minigraph,
paths: () => store.getState().paths,
registration: () => store.getState().registration,
upnp: () => store.getState().upnp,
};

View File

@@ -1,22 +0,0 @@
import { writeFileSync } from 'fs';
import type { ConfigType } from '@app/core/utils/files/config-file-normalizer.js';
import { logger } from '@app/core/log.js';
import { getWriteableConfig } from '@app/core/utils/files/config-file-normalizer.js';
import { safelySerializeObjectToIni } from '@app/core/utils/files/safe-ini-serializer.js';
import { startAppListening } from '@app/store/listeners/listener-middleware.js';
import { configUpdateActionsFlash, configUpdateActionsMemory } from '@app/store/modules/config.js';
export const enableConfigFileListener = (mode: ConfigType) => () =>
startAppListening({
matcher: mode === 'flash' ? configUpdateActionsFlash : configUpdateActionsMemory,
async effect(_, { getState }) {
const { paths, config } = getState();
const pathToWrite =
mode === 'flash' ? paths['myservers-config'] : paths['myservers-config-states'];
const writeableConfig = getWriteableConfig(config, mode);
const serializedConfig = safelySerializeObjectToIni(writeableConfig);
logger.debug('Writing updated config to %s', pathToWrite);
writeFileSync(pathToWrite, serializedConfig);
},
});

View File

@@ -5,9 +5,6 @@ import { addListener, createListenerMiddleware } from '@reduxjs/toolkit';
import { type AppDispatch, type RootState } from '@app/store/index.js';
import { enableArrayEventListener } from '@app/store/listeners/array-event-listener.js';
import { enableConfigFileListener } from '@app/store/listeners/config-listener.js';
import { enableUpnpListener } from '@app/store/listeners/upnp-listener.js';
import { enableVersionListener } from '@app/store/listeners/version-listener.js';
export const listenerMiddleware = createListenerMiddleware();
@@ -21,9 +18,5 @@ export const addAppListener = addListener as TypedAddListener<RootState, AppDisp
export const startMiddlewareListeners = () => {
// Begin listening for events
enableConfigFileListener('flash')();
enableConfigFileListener('memory')();
enableUpnpListener();
enableVersionListener();
enableArrayEventListener();
};

View File

@@ -1,68 +0,0 @@
import { isAnyOf } from '@reduxjs/toolkit';
import { upnpLogger } from '@app/core/log.js';
import { type RootState } from '@app/store/index.js';
import { startAppListening } from '@app/store/listeners/listener-middleware.js';
import { loadConfigFile } from '@app/store/modules/config.js';
import { loadSingleStateFile, loadStateFiles } from '@app/store/modules/emhttp.js';
import { disableUpnp, enableUpnp } from '@app/store/modules/upnp.js';
import { FileLoadStatus } from '@app/store/types.js';
// FLAG for review: make sure we replace this
const shouldUpnpBeEnabled = (state: RootState | null): boolean => {
if (
state?.config.status !== FileLoadStatus.LOADED ||
state?.emhttp.status !== FileLoadStatus.LOADED
) {
return false;
}
const { useUpnp } = state.emhttp.var;
const { upnpEnabled, wanaccess } = state.config.remote;
return useUpnp && upnpEnabled === 'yes' && wanaccess === 'yes';
};
const isStateOrConfigUpdate = isAnyOf(
loadConfigFile.fulfilled,
loadSingleStateFile.fulfilled,
loadStateFiles.fulfilled
// setupRemoteAccessThunk.fulfilled
);
export const enableUpnpListener = () =>
startAppListening({
predicate(action, currentState, previousState) {
// @TODO: One of our actions is incorrectly configured. Sometimes the action is an anonymous function. We need to fix this.
if (
(isStateOrConfigUpdate(action) || !action?.type) &&
shouldUpnpBeEnabled(currentState) !== shouldUpnpBeEnabled(previousState)
) {
return true;
}
return false;
},
async effect(_, { getState, dispatch }) {
const state = getState();
const {
config: {
remote: { wanport },
},
emhttp: {
var: { portssl },
},
} = getState();
upnpLogger.info(
'UPNP Enabled: (%s) Wan Port: [%s]',
shouldUpnpBeEnabled(state),
wanport === '' ? 'Will Generate New WAN Port' : wanport
);
if (shouldUpnpBeEnabled(state)) {
await dispatch(enableUpnp({ wanport, portssl }));
} else {
await dispatch(disableUpnp());
}
},
});

View File

@@ -1,28 +0,0 @@
import { logger } from '@app/core/log.js';
import { API_VERSION } from '@app/environment.js';
import { startAppListening } from '@app/store/listeners/listener-middleware.js';
import { updateUserConfig } from '@app/store/modules/config.js';
import { FileLoadStatus } from '@app/store/types.js';
export const enableVersionListener = () =>
startAppListening({
predicate(_, currentState) {
if (
currentState.config.status === FileLoadStatus.LOADED &&
(currentState.config.api.version === '' ||
currentState.config.api.version !== API_VERSION)
) {
logger.trace('Config Loaded, setting API Version in myservers.cfg to ', API_VERSION);
return true;
}
return false;
},
async effect(_, { dispatch }) {
dispatch(
updateUserConfig({
api: { version: API_VERSION },
})
);
},
});

View File

@@ -1,332 +0,0 @@
import { F_OK } from 'constants';
import { writeFileSync } from 'fs';
import { access } from 'fs/promises';
import type { PayloadAction } from '@reduxjs/toolkit';
import { createAsyncThunk, createSlice, isAnyOf } from '@reduxjs/toolkit';
import { isEqual, merge } from 'lodash-es';
import { logger } from '@app/core/log.js';
import { getWriteableConfig } from '@app/core/utils/files/config-file-normalizer.js';
import { safelySerializeObjectToIni } from '@app/core/utils/files/safe-ini-serializer.js';
import { parseConfig } from '@app/core/utils/misc/parse-config.js';
import { NODE_ENV } from '@app/environment.js';
import { setGraphqlConnectionStatus } from '@app/store/actions/set-minigraph-status.js';
// import { setupRemoteAccessThunk } from '@app/store/actions/setup-remote-access.js';
import { type RootState } from '@app/store/index.js';
import { FileLoadStatus } from '@app/store/types.js';
import { RecursivePartial } from '@app/types/index.js';
import { type MyServersConfig, type MyServersConfigMemory } from '@app/types/my-servers-config.js';
import { MinigraphStatus } from '@app/unraid-api/graph/resolvers/cloud/cloud.model.js';
// import { DynamicRemoteAccessType } from '@app/unraid-api/graph/resolvers/connect/connect.model.js';
import { Owner } from '@app/unraid-api/graph/resolvers/owner/owner.model.js';
export type SliceState = {
status: FileLoadStatus;
nodeEnv: string;
} & MyServersConfigMemory;
export const initialState: SliceState = {
status: FileLoadStatus.UNLOADED,
nodeEnv: NODE_ENV,
remote: {
wanaccess: '',
wanport: '',
upnpEnabled: '',
apikey: '',
localApiKey: '',
email: '',
username: '',
avatar: '',
regWizTime: '',
accesstoken: '',
idtoken: '',
refreshtoken: '',
allowedOrigins: '',
dynamicRemoteAccessType: 'DISABLED',
ssoSubIds: '',
},
local: {
sandbox: 'no',
},
api: {
extraOrigins: '',
version: '',
},
connectionStatus: {
minigraph: MinigraphStatus.PRE_INIT,
upnpStatus: '',
},
} as const;
export const loginUser = createAsyncThunk<
Pick<MyServersConfig['remote'], 'email' | 'avatar' | 'username' | 'apikey' | 'localApiKey'>,
Pick<MyServersConfig['remote'], 'email' | 'avatar' | 'username' | 'apikey' | 'localApiKey'>,
{ state: RootState }
>('config/login-user', async (userInfo) => {
logger.info('Logging in user: %s', userInfo.username);
const { pubsub, PUBSUB_CHANNEL } = await import('@app/core/pubsub.js');
const owner: Owner = {
username: userInfo.username,
avatar: userInfo.avatar,
url: '',
};
await pubsub.publish(PUBSUB_CHANNEL.OWNER, { owner });
return userInfo;
});
export const logoutUser = createAsyncThunk<void, { reason?: string }, { state: RootState }>(
'config/logout-user',
async ({ reason }) => {
logger.warn('invoked legacy logoutUser. no action taken.');
}
);
/**
* Load the myservers.cfg into the store. Returns null if the state after loading doesn't change
*
* Note: If the file doesn't exist this will fallback to default values.
*/
enum CONFIG_LOAD_ERROR {
CONFIG_EQUAL = 'CONFIG_EQUAL',
CONFIG_CORRUPTED = 'CONFIG_CORRUPTED',
}
type LoadFailureWithConfig = {
type: CONFIG_LOAD_ERROR.CONFIG_CORRUPTED;
error: Error | null;
config: MyServersConfig;
};
type LoadFailureConfigEqual = {
type: CONFIG_LOAD_ERROR.CONFIG_EQUAL;
};
type ConfigRejectedValues = LoadFailureConfigEqual | LoadFailureWithConfig;
export const loadConfigFile = createAsyncThunk<
MyServersConfig,
string | undefined,
{
state: RootState;
rejectValue: ConfigRejectedValues;
}
>('config/load-config-file', async (filePath, { getState, rejectWithValue }) => {
try {
const { paths, config } = getState();
const path = filePath ?? paths['myservers-config'];
const fileExists = await access(path, F_OK)
.then(() => true)
.catch(() => false);
if (!fileExists) {
throw new Error('Config File Missing');
}
const newConfigFile = getWriteableConfig(
parseConfig<MyServersConfig>({ filePath: path, type: 'ini' }),
'flash'
);
const isNewlyLoadedConfigEqual = isEqual(newConfigFile, getWriteableConfig(config, 'flash'));
if (isNewlyLoadedConfigEqual) {
logger.warn('Not loading config because it is the same as before');
return rejectWithValue({
type: CONFIG_LOAD_ERROR.CONFIG_EQUAL,
});
}
return newConfigFile;
} catch (error: unknown) {
logger.warn('Config file is corrupted with error: %o - recreating config', error);
const newConfig = getWriteableConfig(initialState, 'flash');
newConfig.remote.wanaccess = 'no';
const serializedConfig = safelySerializeObjectToIni(newConfig);
writeFileSync(getState().paths['myservers-config'], serializedConfig);
return rejectWithValue({
type: CONFIG_LOAD_ERROR.CONFIG_CORRUPTED,
error: error instanceof Error ? error : new Error('Unknown Error'),
config: newConfig,
});
}
});
export const config = createSlice({
name: 'config',
initialState,
reducers: {
updateUserConfig(state, action: PayloadAction<RecursivePartial<MyServersConfig>>) {
return merge(state, action.payload);
},
updateAccessTokens(
state,
action: PayloadAction<
Partial<
Pick<
Pick<MyServersConfig, 'remote'>['remote'],
'accesstoken' | 'refreshtoken' | 'idtoken'
>
>
>
) {
return merge(state, { remote: action.payload });
},
updateAllowedOrigins(state, action: PayloadAction<string[]>) {
state.api.extraOrigins = action.payload.join(', ');
},
setUpnpState(
state,
action: PayloadAction<{
enabled?: 'no' | 'yes' | 'auto';
status?: string | null;
}>
) {
if (action.payload.enabled) {
state.remote.upnpEnabled = action.payload.enabled;
}
if (action.payload.status) {
state.connectionStatus.upnpStatus = action.payload.status;
}
},
setWanPortToValue(state, action: PayloadAction<number>) {
logger.debug('Wan port set to %s', action.payload);
state.remote.wanport = String(action.payload);
},
setWanAccess(state, action: PayloadAction<'yes' | 'no'>) {
state.remote.wanaccess = action.payload;
},
// addSsoUser(state, action: PayloadAction<string>) {
// // First check if state already has ID, otherwise append it
// if (state.remote.ssoSubIds.includes(action.payload)) {
// return;
// }
// const stateAsArray = state.remote.ssoSubIds.split(',').filter((id) => id !== '');
// stateAsArray.push(action.payload);
// state.remote.ssoSubIds = stateAsArray.join(',');
// },
setSsoUsers(state, action: PayloadAction<string[]>) {
state.remote.ssoSubIds = action.payload.filter((id) => id).join(',');
},
// removeSsoUser(state, action: PayloadAction<string | null>) {
// if (action.payload === null) {
// state.remote.ssoSubIds = '';
// return;
// }
// if (!state.remote.ssoSubIds.includes(action.payload)) {
// return;
// }
// const stateAsArray = state.remote.ssoSubIds.split(',').filter((id) => id !== action.payload);
// state.remote.ssoSubIds = stateAsArray.join(',');
// },
setLocalApiKey(state, action: PayloadAction<string | null>) {
state.remote.localApiKey = action.payload ?? '';
},
},
extraReducers(builder) {
builder.addCase(loadConfigFile.pending, (state) => {
state.status = FileLoadStatus.LOADING;
});
builder.addCase(loadConfigFile.fulfilled, (state, action) => {
if (action.payload) {
merge(state, action.payload, { status: FileLoadStatus.LOADED });
} else {
state.status = FileLoadStatus.LOADED;
}
});
builder.addCase(loadConfigFile.rejected, (state, action) => {
switch (action.payload?.type) {
case CONFIG_LOAD_ERROR.CONFIG_EQUAL:
logger.debug('Configs equivalent');
state.status = FileLoadStatus.LOADED;
break;
case CONFIG_LOAD_ERROR.CONFIG_CORRUPTED:
logger.debug('Config File Load Failed - %o', action.payload.error);
merge(state, action.payload.config);
state.status = FileLoadStatus.LOADED;
break;
default:
logger.error('Config File Load Failed', action.error);
}
});
builder.addCase(loginUser.fulfilled, (state, action) => {
merge(state, {
remote: {
apikey: action.payload.apikey,
localApiKey: action.payload.localApiKey,
email: action.payload.email,
username: action.payload.username,
avatar: action.payload.avatar,
},
});
});
builder.addCase(logoutUser.fulfilled, (state) => {
merge(state, {
remote: {
apikey: '',
localApiKey: '',
avatar: '',
email: '',
username: '',
idtoken: '',
accessToken: '',
refreshToken: '',
// dynamicRemoteAccessType: DynamicRemoteAccessType.DISABLED,
},
});
});
builder.addCase(setGraphqlConnectionStatus, (state, action) => {
state.connectionStatus.minigraph = action.payload.status;
});
// builder.addCase(setupRemoteAccessThunk.fulfilled, (state, action) => {
// state.remote.wanaccess = action.payload.wanaccess;
// state.remote.dynamicRemoteAccessType = action.payload.dynamicRemoteAccessType;
// state.remote.wanport = action.payload.wanport;
// state.remote.upnpEnabled = action.payload.upnpEnabled;
// });
},
});
const { actions, reducer } = config;
export const {
// addSsoUser,
setSsoUsers,
updateUserConfig,
updateAccessTokens,
updateAllowedOrigins,
setUpnpState,
setWanPortToValue,
setWanAccess,
// removeSsoUser,
setLocalApiKey,
} = actions;
/**
* Actions that should trigger a flash write
*/
export const configUpdateActionsFlash = isAnyOf(
// addSsoUser,
setSsoUsers,
updateUserConfig,
updateAccessTokens,
updateAllowedOrigins,
setUpnpState,
setWanPortToValue,
setWanAccess,
// setupRemoteAccessThunk.fulfilled,
logoutUser.fulfilled,
loginUser.fulfilled,
// removeSsoUser,
setLocalApiKey
);
/**
* Actions that should trigger a memory write
*/
export const configUpdateActionsMemory = isAnyOf(configUpdateActionsFlash, setGraphqlConnectionStatus);
export const configReducer = reducer;

View File

@@ -1,91 +0,0 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import { KEEP_ALIVE_INTERVAL_MS } from '@app/consts.js';
import { minigraphLogger } from '@app/core/log.js';
import { setGraphqlConnectionStatus } from '@app/store/actions/set-minigraph-status.js';
import { loginUser, logoutUser } from '@app/store/modules/config.js';
import { MinigraphStatus } from '@app/unraid-api/graph/resolvers/cloud/cloud.model.js';
export type MinigraphClientState = {
status: MinigraphStatus;
error: string | null;
lastPing: number | null;
selfDisconnectedSince: number | null;
timeout: number | null;
timeoutStart: number | null;
};
const initialState: MinigraphClientState = {
status: MinigraphStatus.PRE_INIT,
error: null,
lastPing: null,
selfDisconnectedSince: null,
timeout: null,
timeoutStart: null,
};
export const mothership = createSlice({
name: 'mothership',
initialState,
reducers: {
setMothershipTimeout(state, action: PayloadAction<number>) {
state.timeout = action.payload;
state.timeoutStart = Date.now();
},
receivedMothershipPing(state) {
state.lastPing = Date.now();
},
setSelfDisconnected(state) {
minigraphLogger.error(
`Received disconnect event for own server, waiting for ${
KEEP_ALIVE_INTERVAL_MS / 1_000
} seconds before setting disconnected`
);
state.selfDisconnectedSince = Date.now();
},
setSelfReconnected(state) {
minigraphLogger.error(
'Received connected event for own server, clearing disconnection timeout'
);
state.selfDisconnectedSince = null;
},
},
extraReducers(builder) {
builder.addCase(setGraphqlConnectionStatus, (state, action) => {
minigraphLogger.debug('GraphQL Connection Status: %o', action.payload);
state.status = action.payload.status;
state.error = action.payload.error;
if (
[MinigraphStatus.CONNECTED, MinigraphStatus.CONNECTING].includes(action.payload.status)
) {
state.error = null;
state.timeout = null;
state.lastPing = null;
state.selfDisconnectedSince = null;
state.timeoutStart = null;
}
});
builder.addCase(loginUser.pending, (state) => {
state.timeout = null;
state.timeoutStart = null;
state.lastPing = null;
state.selfDisconnectedSince = null;
state.status = MinigraphStatus.PRE_INIT;
state.error = 'Connecting - refresh the page for an updated status.';
});
builder.addCase(logoutUser.pending, (state) => {
state.error = null;
state.timeout = null;
state.lastPing = null;
state.selfDisconnectedSince = null;
state.timeoutStart = null;
state.status = MinigraphStatus.PRE_INIT;
});
},
});
export const { setMothershipTimeout, receivedMothershipPing, setSelfDisconnected, setSelfReconnected } =
mothership.actions;
export const mothershipReducer = mothership.reducer;

View File

@@ -49,7 +49,6 @@ const initialState = {
resolvePath(process.env.PATHS_STATES ?? ('/usr/local/emhttp/state/' as const)),
'myservers.cfg' as const
),
'myservers-env': '/boot/config/plugins/dynamix.my.servers/env' as const,
'myservers-keepalive':
process.env.PATHS_MY_SERVERS_FB ??
('/boot/config/plugins/dynamix.my.servers/fb_keepalive' as const),

View File

@@ -1,215 +0,0 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import type { Mapping } from '@runonflux/nat-upnp';
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { upnpLogger } from '@app/core/log.js';
import { toNumberOrNull } from '@app/core/utils/casting.js';
import { type AppDispatch, type RootState } from '@app/store/index.js';
import { setUpnpState, setWanPortToValue } from '@app/store/modules/config.js';
import {
getUpnpMappings,
getWanPortForUpnp,
removeUpnpLease,
renewUpnpLease,
} from '@app/upnp/helpers.js';
import { initUpnpJobs, stopUpnpJobs } from '@app/upnp/jobs.js';
interface UpnpState {
upnpEnabled: boolean;
wanPortForUpnp: number | null;
localPortForUpnp: number | null;
errors: {
renewal: string | null;
removal: string | null;
mapping: string | null;
};
mappings: Mapping[];
renewalJobRunning: boolean;
}
export const initialState: UpnpState = {
upnpEnabled: false,
errors: {
removal: null,
renewal: null,
mapping: null,
},
wanPortForUpnp: null,
localPortForUpnp: null,
mappings: [],
renewalJobRunning: false,
};
export type LeaseRenewalArgs = { localPortForUpnp: number; wanPortForUpnp: number };
export type UpnpEnableReturnValue = Pick<
UpnpState,
'renewalJobRunning' | 'wanPortForUpnp' | 'localPortForUpnp'
>;
type EnableUpnpThunkArgs = { portssl: number; wanport?: string } | void;
/**
* Return if the removal or renewal set failed, this indicates that an error was probably fatal
* @param errors
* @returns
*/
export const upnpStoreHasFatalError = (errors: UpnpState['errors'] | null): boolean =>
errors ? errors.removal !== null || errors.renewal !== null : false;
/*
* Choose port to use - if we pass arguments it means we're re-initing this function, so ignore upnp.wanPortForUpnp
* If we don't pass args, use the saved WAN port since that means the job is running
*/
const getWanPortToUse = async ({
leaseRenewalArgs,
wanPortArgAsNumber,
wanPortForUpnp,
dispatch,
}: {
leaseRenewalArgs: EnableUpnpThunkArgs;
wanPortArgAsNumber: number | null;
wanPortForUpnp: null | number;
dispatch: AppDispatch;
}): Promise<number | null> => {
if (leaseRenewalArgs) {
if (wanPortArgAsNumber) {
return wanPortArgAsNumber;
}
const currentMappings = await getUpnpMappings();
const newPort = getWanPortForUpnp(currentMappings);
if (newPort) {
dispatch(setWanPortToValue(newPort));
}
return newPort;
}
return wanPortForUpnp;
};
export const enableUpnp = createAsyncThunk<
UpnpEnableReturnValue,
EnableUpnpThunkArgs,
{ state: RootState; dispatch: AppDispatch }
>('upnp/enable', async (leaseRenewalArgs, { getState, dispatch }) => {
const { upnp, emhttp } = getState();
const wanPortArgAsNumber = leaseRenewalArgs?.wanport
? toNumberOrNull(leaseRenewalArgs?.wanport)
: null;
// If the wan port changes we try to negotiate this by removing the old lease first
if (
leaseRenewalArgs &&
upnp.wanPortForUpnp &&
upnp.localPortForUpnp &&
(wanPortArgAsNumber !== upnp.wanPortForUpnp ||
leaseRenewalArgs.portssl !== upnp.localPortForUpnp)
) {
try {
await removeUpnpLease({
wanPortForUpnp: upnp.wanPortForUpnp,
localPortForUpnp: upnp.localPortForUpnp,
});
} catch (error: unknown) {
upnpLogger.warn(
`Caught error [${error instanceof Error ? error.message : 'N/A'}] when removing lease, could be non-fatal, so continuing`
);
}
}
// Start the renewal Job if it's not already running. When run from inside a job this will return true
const renewalJobRunning = upnp.renewalJobRunning ? true : initUpnpJobs();
const wanPortToUse = await getWanPortToUse({
leaseRenewalArgs,
wanPortForUpnp: upnp.wanPortForUpnp,
dispatch,
wanPortArgAsNumber,
});
const localPortToUse = leaseRenewalArgs ? leaseRenewalArgs.portssl : upnp.localPortForUpnp;
if (wanPortToUse && localPortToUse) {
try {
await renewUpnpLease({
localPortForUpnp: localPortToUse,
wanPortForUpnp: wanPortToUse,
serverName: emhttp?.var?.name,
});
const today = new Date();
const todayFormatted = `${today.toLocaleDateString()} ${today.toLocaleTimeString()}`;
dispatch(
setUpnpState({
status: `Success: UPNP Lease Renewed [${todayFormatted}] Public Port [${wanPortToUse}] Local Port [${localPortToUse}]`,
})
);
return { renewalJobRunning, wanPortForUpnp: wanPortToUse, localPortForUpnp: localPortToUse };
} catch (error: unknown) {
const message = `Error: Failed Opening UPNP Public Port [${wanPortToUse}] Local Port [${localPortToUse}] Message: [${error instanceof Error ? error.message : 'N/A'}]`;
dispatch(setUpnpState({ enabled: 'no', status: message }));
throw new Error(message);
}
}
throw new Error('No WAN port found, disabling UPNP');
});
export const disableUpnp = createAsyncThunk<{ renewalJobRunning: boolean }, void, { state: RootState }>(
'upnp/disable',
async (_, { dispatch, getState }) => {
const {
upnp: { localPortForUpnp, wanPortForUpnp },
} = getState();
const renewalJobRunning = stopUpnpJobs();
if (localPortForUpnp && wanPortForUpnp) {
try {
await removeUpnpLease({ localPortForUpnp, wanPortForUpnp });
dispatch(setUpnpState({ enabled: 'no', status: 'UPNP Disabled' }));
} catch (error: unknown) {
upnpLogger.warn(
`Failed to remove UPNP Binding with Error [${error instanceof Error ? error.message : 'N/A'}]`
);
}
}
return { renewalJobRunning };
}
);
export const upnp = createSlice({
name: 'upnp',
initialState,
reducers: {
updateMappings(state, action: PayloadAction<Mapping[]>) {
state.mappings = action.payload;
},
},
extraReducers(builder) {
builder.addCase(enableUpnp.pending, (state) => {
state.upnpEnabled = true;
});
builder.addCase(enableUpnp.fulfilled, (state, action) => {
state.localPortForUpnp = action.payload.localPortForUpnp;
state.wanPortForUpnp = action.payload.wanPortForUpnp;
state.renewalJobRunning = action.payload.renewalJobRunning;
});
builder.addCase(enableUpnp.rejected, (state, action) => {
upnpLogger.warn('Failed to renew UPNP Lease with Error %o', action.error);
state.errors.renewal = action.error.message ?? 'Undefined Error When Renewing UPNP Lease';
});
builder.addCase(disableUpnp.fulfilled, (state, action) => {
state.renewalJobRunning = action.payload.renewalJobRunning;
state.wanPortForUpnp = null;
state.localPortForUpnp = null;
state.upnpEnabled = false;
});
},
});
const { actions, reducer } = upnp;
export const { updateMappings } = actions;
export const upnpReducer = reducer;

View File

@@ -1,25 +1,19 @@
import { combineReducers, UnknownAction } from '@reduxjs/toolkit';
import { resetStore } from '@app/store/actions/reset-store.js';
import { configReducer } from '@app/store/modules/config.js';
import { dynamix } from '@app/store/modules/dynamix.js';
import { emhttp } from '@app/store/modules/emhttp.js';
import { mothershipReducer } from '@app/store/modules/minigraph.js';
import { paths } from '@app/store/modules/paths.js';
import { registrationReducer } from '@app/store/modules/registration.js';
import { upnp } from '@app/store/modules/upnp.js';
/**
* Root reducer that combines all slice reducers and handles the reset action.
* When the reset action is dispatched, all slices will be reset to their initial state.
*/
const appReducer = combineReducers({
config: configReducer,
minigraph: mothershipReducer,
paths: paths.reducer,
emhttp: emhttp.reducer,
registration: registrationReducer,
upnp: upnp.reducer,
dynamix: dynamix.reducer,
});

View File

@@ -1,39 +0,0 @@
import { writeFileSync } from 'fs';
import { join } from 'path';
import { isEqual } from 'lodash-es';
import type { RootState } from '@app/store/index.js';
import { NODE_ENV } from '@app/environment.js';
import { store } from '@app/store/index.js';
import { syncRegistration } from '@app/store/sync/registration-sync.js';
import { FileLoadStatus } from '@app/store/types.js';
export const startStoreSync = async () => {
// The last state is stored so we don't end up in a loop of writing -> reading -> writing
let lastState: RootState | null = null;
// Update cfg when store changes
store.subscribe(async () => {
const state = store.getState();
// Config dependent options, wait until config loads to execute
if (state.config.status === FileLoadStatus.LOADED) {
// Update registration
await syncRegistration(lastState);
}
if (
NODE_ENV === 'development' &&
!isEqual(state, lastState) &&
state.paths['myservers-config-states']
) {
writeFileSync(join(state.paths.states, 'config.log'), JSON.stringify(state.config, null, 2));
writeFileSync(
join(state.paths.states, 'graphql.log'),
JSON.stringify(state.minigraph, null, 2)
);
}
lastState = state;
});
};

View File

@@ -1,22 +0,0 @@
import { writeFileSync } from 'fs';
import type { ConfigType } from '@app/core/utils/files/config-file-normalizer.js';
import { logger } from '@app/core/log.js';
import { getWriteableConfig } from '@app/core/utils/files/config-file-normalizer.js';
import { safelySerializeObjectToIni } from '@app/core/utils/files/safe-ini-serializer.js';
import { store } from '@app/store/index.js';
import { FileLoadStatus } from '@app/store/types.js';
export const writeConfigSync = (mode: ConfigType) => {
const { config, paths } = store.getState();
if (config.status !== FileLoadStatus.LOADED) {
logger.warn('Configs not loaded, unable to write sync');
return;
}
const writeableConfig = getWriteableConfig(config, mode);
const path = mode === 'flash' ? paths['myservers-config'] : paths['myservers-config-states'];
const serializedConfig = safelySerializeObjectToIni(writeableConfig);
writeFileSync(path, serializedConfig);
};

View File

@@ -1,69 +0,0 @@
import { isEqual } from 'lodash-es';
import type { StoreSubscriptionHandler } from '@app/store/types.js';
import { logger } from '@app/core/log.js';
import { pubsub, PUBSUB_CHANNEL } from '@app/core/pubsub.js';
import { store } from '@app/store/index.js';
import { FileLoadStatus } from '@app/store/types.js';
export type RegistrationEvent = {
registration: {
guid: string;
type: string;
state: string;
keyFile: {
location: string;
contents: null;
};
};
};
export const createRegistrationEvent = (
state: Parameters<StoreSubscriptionHandler>[0]
): RegistrationEvent | null => {
// Var state isn't loaded
if (state === null || Object.keys(state.emhttp.var).length === 0) return null;
const event = {
registration: {
guid: state.emhttp.var.regGuid,
type: state.emhttp.var.regTy.toUpperCase(),
state: state.emhttp.var.regState,
keyFile: {
location: state.emhttp.var.regFile,
contents: state.registration.keyFile,
},
},
};
return event;
};
export const syncRegistration: StoreSubscriptionHandler = async (lastState) => {
try {
// Skip until we have the key and emhttp states loaded
const { registration, emhttp } = store.getState();
if (registration.status !== FileLoadStatus.LOADED) return;
if (emhttp.status !== FileLoadStatus.LOADED) return;
const lastEvent = createRegistrationEvent(lastState);
const currentEvent = createRegistrationEvent(store.getState());
// Skip if either event resolved to null
if (lastEvent === null || currentEvent === null) return;
// Skip this if it's the same as the last one
if (isEqual(lastEvent, currentEvent)) return;
logger.debug('Registration was updated, publishing event');
// Publish to graphql
await pubsub.publish(PUBSUB_CHANNEL.REGISTRATION, currentEvent);
} catch (error: unknown) {
if (!(error instanceof Error))
throw new Error(
`Failed publishing registration event with unknown error "${String(error)}"`
);
logger.error('Failed publishing registration event with "%s"', error.message);
}
};

View File

@@ -1,72 +0,0 @@
import { z } from 'zod';
import { MinigraphStatus } from '@app/unraid-api/graph/resolvers/cloud/cloud.model.js';
// Define Zod schemas
const ApiConfigSchema = z.object({
version: z.string(),
extraOrigins: z.string(),
});
const RemoteConfigSchema = z.object({
wanaccess: z.string(),
wanport: z.string(),
upnpEnabled: z.string(),
apikey: z.string(),
localApiKey: z.string(),
email: z.string(),
username: z.string(),
avatar: z.string(),
regWizTime: z.string(),
accesstoken: z.string(),
idtoken: z.string(),
refreshtoken: z.string(),
dynamicRemoteAccessType: z.string(),
ssoSubIds: z
.string()
.transform((val) => {
// If valid, return as is
if (val === '' || val.split(',').every((id) => id.trim().match(/^[a-zA-Z0-9-]+$/))) {
return val;
}
// Otherwise, replace with an empty string
return '';
})
.refine(
(val) => val === '' || val.split(',').every((id) => id.trim().match(/^[a-zA-Z0-9-]+$/)),
{
message:
'ssoSubIds must be empty or a comma-separated list of alphanumeric strings with dashes',
}
),
});
const LocalConfigSchema = z.object({
sandbox: z.enum(['yes', 'no']).default('no'),
});
// Base config schema
export const MyServersConfigSchema = z
.object({
api: ApiConfigSchema,
local: LocalConfigSchema,
remote: RemoteConfigSchema,
})
.strip();
// Memory config schema
export const ConnectionStatusSchema = z.object({
minigraph: z.nativeEnum(MinigraphStatus),
upnpStatus: z.string().nullable().optional(),
});
export const MyServersConfigMemorySchema = MyServersConfigSchema.extend({
connectionStatus: ConnectionStatusSchema,
remote: RemoteConfigSchema.extend({
allowedOrigins: z.string(),
}),
}).strip();
// Infer and export types from Zod schemas
export type MyServersConfig = z.infer<typeof MyServersConfigSchema>;
export type MyServersConfigMemory = z.infer<typeof MyServersConfigMemorySchema>;

View File

@@ -0,0 +1,256 @@
import { INestApplication } from '@nestjs/common';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
import { Test, TestingModule } from '@nestjs/testing';
import { AuthZGuard } from 'nest-authz';
import request from 'supertest';
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import { loadDynamixConfigFile } from '@app/store/actions/load-dynamix-config-file.js';
import { store } from '@app/store/index.js';
import { loadStateFiles } from '@app/store/modules/emhttp.js';
import { AppModule } from '@app/unraid-api/app/app.module.js';
import { AuthService } from '@app/unraid-api/auth/auth.service.js';
import { AuthenticationGuard } from '@app/unraid-api/auth/authentication.guard.js';
import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
// Mock external system boundaries that we can't control in tests
vi.mock('dockerode', () => {
return {
default: vi.fn().mockImplementation(() => ({
listContainers: vi.fn().mockResolvedValue([
{
Id: 'test-container-1',
Names: ['/test-container'],
State: 'running',
Status: 'Up 5 minutes',
Image: 'test:latest',
Command: 'node server.js',
Created: Date.now() / 1000,
Ports: [
{
IP: '0.0.0.0',
PrivatePort: 3000,
PublicPort: 3000,
Type: 'tcp',
},
],
Labels: {},
HostConfig: {
NetworkMode: 'bridge',
},
NetworkSettings: {
Networks: {},
},
Mounts: [],
},
]),
getContainer: vi.fn().mockImplementation((id) => ({
inspect: vi.fn().mockResolvedValue({
Id: id,
Name: '/test-container',
State: { Running: true },
Config: { Image: 'test:latest' },
}),
})),
listImages: vi.fn().mockResolvedValue([]),
listNetworks: vi.fn().mockResolvedValue([]),
listVolumes: vi.fn().mockResolvedValue({ Volumes: [] }),
})),
};
});
// Mock external command execution
vi.mock('execa', () => ({
execa: vi.fn().mockImplementation((cmd) => {
if (cmd === 'whoami') {
return Promise.resolve({ stdout: 'testuser' });
}
return Promise.resolve({ stdout: 'mocked output' });
}),
}));
// Mock child_process for services that spawn processes
vi.mock('node:child_process', () => ({
spawn: vi.fn(() => ({
on: vi.fn(),
kill: vi.fn(),
stdout: { on: vi.fn() },
stderr: { on: vi.fn() },
})),
}));
// Mock file system operations that would fail in test environment
vi.mock('node:fs/promises', async (importOriginal) => {
const actual = await importOriginal<typeof import('fs/promises')>();
return {
...actual,
readFile: vi.fn().mockResolvedValue(''),
writeFile: vi.fn().mockResolvedValue(undefined),
mkdir: vi.fn().mockResolvedValue(undefined),
access: vi.fn().mockResolvedValue(undefined),
stat: vi.fn().mockResolvedValue({ isFile: () => true }),
readdir: vi.fn().mockResolvedValue([]),
rename: vi.fn().mockResolvedValue(undefined),
unlink: vi.fn().mockResolvedValue(undefined),
};
});
// Mock fs module for synchronous operations
vi.mock('node:fs', () => ({
existsSync: vi.fn().mockReturnValue(false),
readFileSync: vi.fn().mockReturnValue(''),
writeFileSync: vi.fn(),
mkdirSync: vi.fn(),
readdirSync: vi.fn().mockReturnValue([]),
}));
describe('AppModule Integration Tests', () => {
let app: NestFastifyApplication;
let moduleRef: TestingModule;
beforeAll(async () => {
// Initialize the dynamix config and state files before creating the module
await store.dispatch(loadDynamixConfigFile());
await store.dispatch(loadStateFiles());
// Debug: Log the CSRF token from the store
const { getters } = await import('@app/store/index.js');
console.log('CSRF Token from store:', getters.emhttp().var.csrfToken);
moduleRef = await Test.createTestingModule({
imports: [AppModule],
})
// Override authentication for tests
.overrideGuard(AuthenticationGuard)
.useValue({
canActivate: () => true,
})
// Override authorization guard
.overrideGuard(AuthZGuard)
.useValue({
canActivate: () => true,
})
// Override AuthService to bypass CSRF validation
.overrideProvider(AuthService)
.useValue({
validateCookiesWithCsrfToken: vi.fn().mockResolvedValue({
id: 'test-user',
name: 'Test User',
roles: ['admin'],
}),
validateApiKeyCasbin: vi.fn().mockResolvedValue({
id: 'test-user',
name: 'Test User',
roles: ['admin'],
}),
getSessionUser: vi.fn().mockResolvedValue({
id: 'test-user',
name: 'Test User',
roles: ['admin'],
}),
})
// Override Redis client
.overrideProvider('REDIS_CLIENT')
.useValue({
get: vi.fn(),
set: vi.fn(),
del: vi.fn(),
connect: vi.fn(),
})
.compile();
app = moduleRef.createNestApplication<NestFastifyApplication>(new FastifyAdapter());
await app.init();
await app.getHttpAdapter().getInstance().ready();
}, 30000);
afterAll(async () => {
if (app) {
await app.close();
}
});
describe('Module Compilation', () => {
it('should successfully compile all modules with proper dependency injection', () => {
expect(moduleRef).toBeDefined();
expect(app).toBeDefined();
});
it('should resolve core services', () => {
const dockerService = moduleRef.get(DockerService);
expect(dockerService).toBeDefined();
});
});
describe('GraphQL API', () => {
it('should expose GraphQL endpoint and handle a simple query', async () => {
// Query for a simpler public endpoint that doesn't require permissions
const query = `
query {
isSSOEnabled
}
`;
const response = await request(app.getHttpServer())
.post('/graphql')
.set('x-csrf-token', '0000000000000000') // Add CSRF token from dev/states/var.ini
.send({ query })
.expect((res) => {
// Log the response for debugging
if (res.status !== 200 || res.body.errors) {
console.error('GraphQL Response:', JSON.stringify(res.body, null, 2));
}
});
expect(response.status).toBe(200);
expect(response.body.errors).toBeUndefined();
expect(response.body.data).toBeDefined();
expect(response.body.data.isSSOEnabled).toBeDefined();
expect(typeof response.body.data.isSSOEnabled).toBe('boolean');
});
it('should execute public theme query', async () => {
const query = `
query {
publicTheme {
name
}
}
`;
const response = await request(app.getHttpServer())
.post('/graphql')
.send({ query })
.expect((res) => {
// Log the response for debugging
if (res.status !== 200 || res.body.errors) {
console.error('GraphQL Response:', JSON.stringify(res.body, null, 2));
}
});
expect(response.status).toBe(200);
// The query may have errors if theme is not configured, but the GraphQL endpoint should still work
expect(response.body).toBeDefined();
// Either we get data or errors, but the endpoint should respond
expect(response.body.data || response.body.errors).toBeDefined();
});
});
describe('Service Integration', () => {
it('should have working service-to-service communication', async () => {
const dockerService = moduleRef.get(DockerService);
// Test that the service can be called and returns expected data structure
const containers = await dockerService.getContainers();
expect(containers).toBeInstanceOf(Array);
// The containers might be empty or cached, just verify structure
if (containers.length > 0) {
expect(containers[0]).toHaveProperty('id');
expect(containers[0]).toHaveProperty('names');
}
});
});
});

View File

@@ -0,0 +1,22 @@
import { Test } from '@nestjs/testing';
import { describe, expect, it } from 'vitest';
import { RestModule } from '@app/unraid-api/rest/rest.module.js';
describe('Module Dependencies Integration', () => {
it('should compile RestModule without dependency injection errors', async () => {
let module;
try {
module = await Test.createTestingModule({
imports: [RestModule],
}).compile();
expect(module).toBeDefined();
} finally {
if (module) {
await module.close();
}
}
});
});

View File

@@ -1,83 +0,0 @@
import { type CorsOptions } from '@nestjs/common/interfaces/external/cors-options.interface.js';
import { GraphQLError } from 'graphql';
import { getAllowedOrigins } from '@app/common/allowed-origins.js';
import { apiLogger } from '@app/core/log.js';
import { BYPASS_CORS_CHECKS } from '@app/environment.js';
import { type CookieService } from '@app/unraid-api/auth/cookie.service.js';
import { FastifyRequest } from '@app/unraid-api/types/fastify.js';
/**
* Returns whether the origin is allowed to access the API.
*
* @throws GraphQLError if the origin is not in the list of allowed origins
* and `BYPASS_CORS_CHECKS` flag is not set.
*/
// note: don't make this function synchronous. throwing will then crash the server.
export async function isOriginAllowed(origin: string | undefined) {
const allowedOrigins = getAllowedOrigins();
if (origin && allowedOrigins.includes(origin)) {
return true;
} else {
apiLogger.debug(`Origin not in allowed origins: ${origin}`);
if (BYPASS_CORS_CHECKS) {
return true;
}
throw new GraphQLError(
'The CORS policy for this site does not allow access from the specified Origin.'
);
}
}
/**------------------------------------------------------------------------
* ? Fastify Cors Config
*
* The fastify cors configuration function is very different from express,
* but Nest.js doesn't have clear docs or types describing this so I'm
* documenting it here.
*
* This takes a fastify app instance and returns a cors config function, instead
* of just the cors config function (which is nest's default behavior).
*------------------------------------------------------------------------**/
/**
* A wrapper function for the fastify CORS configuration, which
* takes a CookieService (i.e. a singleton from Nest.js) and returns a
* fastify CORS config function. This function:
*
* Dynamically determines the CORS config for a request.
*
* - Expects any cookies to be parsed & available on the `cookies` property of the request.
*
* If the request contains a valid unraid session cookie, it is allowed to access
* the API from any origin. Otherwise, the origin must be explicitly listed in
* the `allowedOrigins` config option, or the `BYPASS_PERMISSION_CHECKS` flag
* must be set.
*/
export const configureFastifyCors =
(service: CookieService) =>
// this is the function that nestApp.enableCors() needs when configured to use fastify
() =>
/**
* Our CORS handler function. It dynamically determines the CORS config for a request.
*
* @param req the request object
* @param callback the callback to call with the CORS options
*/
(req: FastifyRequest, callback: (error: Error | null, options: CorsOptions) => void) => {
const { cookies } = req;
if (cookies && typeof cookies === 'object') {
service.hasValidAuthCookie(cookies).then((isValid) => {
if (isValid) {
callback(null, { credentials: true, origin: true });
} else {
callback(null, { credentials: true, origin: isOriginAllowed });
}
});
} else {
callback(null, { credentials: true, origin: isOriginAllowed });
}
};

View File

@@ -8,20 +8,13 @@ import { AuthActionVerb } from 'nest-authz';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { environment } from '@app/environment.js';
import { getters, store } from '@app/store/index.js';
import { updateUserConfig } from '@app/store/modules/config.js';
import { FileLoadStatus } from '@app/store/types.js';
import { getters } from '@app/store/index.js';
import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js';
import {
ApiKey,
ApiKeyWithSecret,
Permission,
} from '@app/unraid-api/graph/resolvers/api-key/api-key.model.js';
import { ApiKey, ApiKeyWithSecret } from '@app/unraid-api/graph/resolvers/api-key/api-key.model.js';
// Mock the store and its modules
vi.mock('@app/store/index.js', () => ({
getters: {
config: vi.fn(),
paths: vi.fn(),
},
store: {
@@ -30,15 +23,6 @@ vi.mock('@app/store/index.js', () => ({
},
}));
vi.mock('@app/store/modules/config.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('@app/store/modules/config.js')>();
return {
...actual,
updateUserConfig: vi.fn(),
setLocalApiKey: vi.fn(),
};
});
// Mock fs/promises
vi.mock('fs/promises', async () => ({
readdir: vi.fn().mockResolvedValue(['key1.json', 'key2.json', 'notakey.txt']),
@@ -116,15 +100,6 @@ describe('ApiKeyService', () => {
'auth-keys': mockBasePath,
} as any);
// Set up default config mock
vi.mocked(getters.config).mockReturnValue({
status: FileLoadStatus.LOADED,
remote: {
apikey: null,
localApiKey: null,
},
} as any);
// Mock ensureDir
vi.mocked(ensureDir).mockResolvedValue();
@@ -142,7 +117,7 @@ describe('ApiKeyService', () => {
describe('initialization', () => {
it('should ensure directory exists', async () => {
const service = new ApiKeyService();
new ApiKeyService();
expect(ensureDirSync).toHaveBeenCalledWith(mockBasePath);
});

View File

@@ -12,9 +12,7 @@ import { AuthActionVerb } from 'nest-authz';
import { v4 as uuidv4 } from 'uuid';
import { environment } from '@app/environment.js';
import { getters, store } from '@app/store/index.js';
import { setLocalApiKey } from '@app/store/modules/config.js';
import { FileLoadStatus } from '@app/store/types.js';
import { getters } from '@app/store/index.js';
import {
AddPermissionInput,
ApiKey,
@@ -117,7 +115,7 @@ export class ApiKeyService implements OnModuleInit {
overwrite = false,
}: {
name: string;
description: string | undefined;
description?: string;
roles?: Role[];
permissions?: Permission[] | AddPermissionInput[];
overwrite?: boolean;
@@ -359,4 +357,59 @@ export class ApiKeyService implements OnModuleInit {
await this.saveApiKey(apiKey);
return apiKey;
}
/**
* Ensures an API key exists, creating it if necessary.
* Used by internal services like Connect and CLI for automatic key management.
*/
public async ensureKey(config: {
name: string;
description: string;
roles: Role[];
legacyNames?: string[];
}): Promise<string> {
// Clean up any legacy keys
if (config.legacyNames && config.legacyNames.length > 0) {
const allKeys = await this.findAll();
const legacyKeys = allKeys.filter((key) => config.legacyNames!.includes(key.name));
if (legacyKeys.length > 0) {
await this.deleteApiKeys(legacyKeys.map((key) => key.id));
this.logger.log(`Deleted legacy API keys: ${config.legacyNames.join(', ')}`);
}
}
// Check if key already exists
const existingKey = this.findByField('name', config.name);
if (existingKey) {
return existingKey.key;
}
// Create new key
const newApiKey = await this.getOrCreateLocalKey(config.name, config.description, config.roles);
this.logger.log(`Created new API key: ${config.name}`);
return newApiKey;
}
/**
* Gets or creates a local API key with the specified name, description, and roles.
*/
public async getOrCreateLocalKey(name: string, description: string, roles: Role[]): Promise<string> {
try {
const apiKey = await this.create({
name,
description,
roles,
overwrite: true,
});
if (!apiKey?.key) {
throw new Error(`Failed to create local API key: ${name}`);
}
return apiKey.key;
} catch (err) {
this.logger.error(`Failed to create local API key ${name}: ${err}`);
throw err;
}
}
}

View File

@@ -11,6 +11,7 @@ import { BASE_POLICY, CASBIN_MODEL } from '@app/unraid-api/auth/casbin/index.js'
import { CookieService, SESSION_COOKIE_CONFIG } from '@app/unraid-api/auth/cookie.service.js';
import { UserCookieStrategy } from '@app/unraid-api/auth/cookie.strategy.js';
import { ServerHeaderStrategy } from '@app/unraid-api/auth/header.strategy.js';
import { AdminKeyService } from '@app/unraid-api/cli/admin-key.service.js';
import { getRequest } from '@app/utils.js';
@Module({
@@ -50,6 +51,7 @@ import { getRequest } from '@app/utils.js';
providers: [
AuthService,
ApiKeyService,
AdminKeyService,
ServerHeaderStrategy,
UserCookieStrategy,
CookieService,

View File

@@ -1,17 +1,21 @@
import { Injectable, Logger } from '@nestjs/common';
import { Injectable, Logger, Optional } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import type { SsoUserService as ISsoUserService } from '@unraid/shared/services/sso.js';
import { GraphQLError } from 'graphql/error/GraphQLError.js';
import type { ApiConfig } from '@app/unraid-api/config/api-config.module.js';
import { UnraidFileModificationService } from '@app/unraid-api/unraid-file-modifier/unraid-file-modifier.service.js';
@Injectable()
export class SsoUserService implements ISsoUserService {
private readonly logger = new Logger(SsoUserService.name);
private ssoSubIdsConfigKey = 'api.ssoSubIds';
constructor(private readonly configService: ConfigService) {}
constructor(
private readonly configService: ConfigService,
@Optional() private readonly fileModificationService?: UnraidFileModificationService
) {}
/**
* Get the current list of SSO user IDs
@@ -48,75 +52,29 @@ export class SsoUserService implements ISsoUserService {
// Update the config
this.configService.set(this.ssoSubIdsConfigKey, userIds);
// Request a restart if there were no SSO users before
return currentUserSet.size === 0;
}
/**
* Add a single SSO user ID
* @param userId - The SSO user ID to add
* @returns true if a restart is required, false otherwise
*/
async addSsoUser(userId: string): Promise<boolean> {
const currentUsers = await this.getSsoUsers();
// If user already exists, no need to update
if (currentUsers.includes(userId)) {
return false;
// Handle file modification if available
if (this.fileModificationService) {
// If going from 0 to 1+ users, apply the SSO modification
if (currentUserSet.size === 0 && newUserSet.size > 0) {
try {
await this.fileModificationService.applyModificationById('sso');
this.logger.log('Applied SSO file modification after adding SSO users');
} catch (error) {
this.logger.error('Failed to apply SSO file modification', error);
}
}
// If going from 1+ to 0 users, rollback the SSO modification
else if (currentUserSet.size > 0 && newUserSet.size === 0) {
try {
await this.fileModificationService.rollbackModificationById('sso');
this.logger.log('Rolled back SSO file modification after removing all SSO users');
} catch (error) {
this.logger.error('Failed to rollback SSO file modification', error);
}
}
}
// Validate user ID
const uuidRegex =
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
if (!uuidRegex.test(userId)) {
throw new GraphQLError(`Invalid SSO user ID: ${userId}`);
}
// Add the new user
const newUsers = [...currentUsers, userId];
this.configService.set(this.ssoSubIdsConfigKey, newUsers);
// Request a restart if there were no SSO users before
return currentUsers.length === 0;
}
/**
* Remove a single SSO user ID
* @param userId - The SSO user ID to remove
* @returns true if a restart is required, false otherwise
*/
async removeSsoUser(userId: string): Promise<boolean> {
const currentUsers = await this.getSsoUsers();
// If user doesn't exist, no need to update
if (!currentUsers.includes(userId)) {
return false;
}
// Remove the user
const newUsers = currentUsers.filter((id) => id !== userId);
this.configService.set(this.ssoSubIdsConfigKey, newUsers);
// Request a restart if this was the last SSO user
return currentUsers.length === 1;
}
/**
* Remove all SSO users
* @returns true if a restart is required, false otherwise
*/
async removeAllSsoUsers(): Promise<boolean> {
const currentUsers = await this.getSsoUsers();
// If no users exist, no need to update
if (currentUsers.length === 0) {
return false;
}
// Remove all users
this.configService.set(this.ssoSubIdsConfigKey, []);
// Request a restart if there were any SSO users
return true;
// No restart required - file modifications are applied immediately
return false;
}
}

View File

@@ -0,0 +1,154 @@
import { Test } from '@nestjs/testing';
import { InquirerService } from 'nest-commander';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { CliInternalClientService } from '@app/unraid-api/cli/internal-client.service.js';
import { LogService } from '@app/unraid-api/cli/log.service.js';
import { RestartCommand } from '@app/unraid-api/cli/restart.command.js';
import { AddSSOUserCommand } from '@app/unraid-api/cli/sso/add-sso-user.command.js';
// Mock services
const mockInternalClient = {
getClient: vi.fn(),
};
const mockLogger = {
info: vi.fn(),
error: vi.fn(),
};
const mockRestartCommand = {
run: vi.fn(),
};
const mockInquirerService = {
prompt: vi.fn(),
};
describe('AddSSOUserCommand', () => {
let command: AddSSOUserCommand;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
AddSSOUserCommand,
{ provide: CliInternalClientService, useValue: mockInternalClient },
{ provide: LogService, useValue: mockLogger },
{ provide: RestartCommand, useValue: mockRestartCommand },
{ provide: InquirerService, useValue: mockInquirerService },
],
}).compile();
command = module.get<AddSSOUserCommand>(AddSSOUserCommand);
// Clear mocks
vi.clearAllMocks();
});
it('should add a new SSO user successfully', async () => {
const mockClient = {
query: vi.fn().mockResolvedValue({
data: {
settings: {
api: {
ssoSubIds: ['existing-user-id'],
},
},
},
}),
mutate: vi.fn().mockResolvedValue({
data: {
updateSettings: {
restartRequired: false,
values: {},
},
},
}),
};
mockInternalClient.getClient.mockResolvedValue(mockClient);
mockInquirerService.prompt.mockResolvedValue({
disclaimer: 'y',
username: 'new-user-id',
});
await command.run([]);
expect(mockClient.query).toHaveBeenCalled();
expect(mockClient.mutate).toHaveBeenCalledWith({
mutation: expect.anything(),
variables: {
input: {
api: {
ssoSubIds: ['existing-user-id', 'new-user-id'],
},
},
},
});
expect(mockLogger.info).toHaveBeenCalledWith('User added: new-user-id');
expect(mockLogger.info).not.toHaveBeenCalledWith('Restarting the API');
expect(mockRestartCommand.run).not.toHaveBeenCalled();
});
it('should not add user if disclaimer is not accepted', async () => {
const mockClient = {
query: vi.fn(),
mutate: vi.fn(),
};
mockInternalClient.getClient.mockResolvedValue(mockClient);
mockInquirerService.prompt.mockResolvedValue({
disclaimer: 'n',
username: 'new-user-id',
});
await command.run([]);
expect(mockClient.query).not.toHaveBeenCalled();
expect(mockClient.mutate).not.toHaveBeenCalled();
expect(mockRestartCommand.run).not.toHaveBeenCalled();
});
it('should not add user if user already exists', async () => {
const mockClient = {
query: vi.fn().mockResolvedValue({
data: {
settings: {
api: {
ssoSubIds: ['existing-user-id'],
},
},
},
}),
mutate: vi.fn(),
};
mockInternalClient.getClient.mockResolvedValue(mockClient);
mockInquirerService.prompt.mockResolvedValue({
disclaimer: 'y',
username: 'existing-user-id',
});
await command.run([]);
expect(mockClient.query).toHaveBeenCalled();
expect(mockClient.mutate).not.toHaveBeenCalled();
expect(mockLogger.error).toHaveBeenCalledWith(
'User existing-user-id already exists in SSO users'
);
expect(mockRestartCommand.run).not.toHaveBeenCalled();
});
it('should handle errors gracefully', async () => {
mockInternalClient.getClient.mockRejectedValue(new Error('Connection failed'));
mockInquirerService.prompt.mockResolvedValue({
disclaimer: 'y',
username: 'new-user-id',
});
await command.run([]);
expect(mockLogger.error).toHaveBeenCalledWith('Error adding user:', expect.any(Error));
});
});

View File

@@ -0,0 +1,405 @@
import { Test } from '@nestjs/testing';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ApiReportService } from '@app/unraid-api/cli/api-report.service.js';
import { CliInternalClientService } from '@app/unraid-api/cli/internal-client.service.js';
import { LogService } from '@app/unraid-api/cli/log.service.js';
import {
CONNECT_STATUS_QUERY,
SERVICES_QUERY,
SYSTEM_REPORT_QUERY,
} from '@app/unraid-api/cli/queries/system-report.query.js';
// Mock Apollo Client
const mockClient = {
query: vi.fn(),
stop: vi.fn(),
};
// Mock internal client service
const mockInternalClientService = {
getClient: vi.fn().mockResolvedValue(mockClient),
clearClient: vi.fn(),
};
// Mock log service
const mockLogService = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
clear: vi.fn(),
};
describe('ApiReportService', () => {
let apiReportService: ApiReportService;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
ApiReportService,
{ provide: LogService, useValue: mockLogService },
{ provide: CliInternalClientService, useValue: mockInternalClientService },
],
}).compile();
apiReportService = module.get<ApiReportService>(ApiReportService);
// Clear mocks
vi.clearAllMocks();
});
describe('generateReport', () => {
const mockSystemData = {
info: {
id: 'info',
machineId: 'test-machine-id',
system: {
manufacturer: 'Test Manufacturer',
model: 'Test Model',
version: '1.0',
sku: 'TEST-SKU',
serial: 'TEST-SERIAL',
uuid: 'test-uuid',
},
versions: {
unraid: '6.12.0',
kernel: '5.19.17',
openssl: '3.0.8',
},
},
config: {
id: 'config',
valid: true,
error: null,
},
server: {
id: 'server',
name: 'Test Server',
},
};
const mockConnectData = {
connect: {
id: 'connect',
dynamicRemoteAccess: {
enabledType: 'STATIC',
runningType: 'STATIC',
error: null,
},
},
};
const mockServicesData = {
services: [
{
id: 'service-cloud',
name: 'cloud',
online: true,
uptime: { timestamp: '2023-01-01T00:00:00Z' },
version: '1.0.0',
},
{
id: 'service-minigraph',
name: 'minigraph',
online: false,
uptime: null,
version: '2.0.0',
},
],
};
it('should generate complete report when API is running and all queries succeed', async () => {
// Configure mock to return different data based on query
mockClient.query.mockImplementation(({ query }) => {
if (query === SYSTEM_REPORT_QUERY) {
return Promise.resolve({ data: mockSystemData });
} else if (query === CONNECT_STATUS_QUERY) {
return Promise.resolve({ data: mockConnectData });
} else if (query === SERVICES_QUERY) {
return Promise.resolve({ data: mockServicesData });
}
return Promise.reject(new Error('Unknown query'));
});
const result = await apiReportService.generateReport(true);
// Verify GraphQL client was called for all queries
expect(mockInternalClientService.getClient).toHaveBeenCalled();
expect(mockClient.query).toHaveBeenCalledWith({
query: SYSTEM_REPORT_QUERY,
});
expect(mockClient.query).toHaveBeenCalledWith({
query: CONNECT_STATUS_QUERY,
});
expect(mockClient.query).toHaveBeenCalledWith({
query: SERVICES_QUERY,
});
// Verify report structure
expect(result).toMatchObject({
timestamp: expect.any(String),
connectionStatus: {
running: 'yes',
},
system: {
id: 'test-uuid',
name: 'Test Server',
version: '6.12.0',
machineId: 'REDACTED',
manufacturer: 'Test Manufacturer',
model: 'Test Model',
},
connect: {
installed: true,
dynamicRemoteAccess: {
enabledType: 'STATIC',
runningType: 'STATIC',
error: null,
},
},
config: {
valid: true,
error: null,
},
services: {
cloud: {
id: 'service-cloud',
name: 'cloud',
online: true,
uptime: { timestamp: '2023-01-01T00:00:00Z' },
version: '1.0.0',
},
minigraph: {
id: 'service-minigraph',
name: 'minigraph',
online: false,
uptime: null,
version: '2.0.0',
},
allServices: [
{
name: 'cloud',
online: true,
version: '1.0.0',
uptime: '2023-01-01T00:00:00Z',
},
{
name: 'minigraph',
online: false,
version: '2.0.0',
uptime: null,
},
],
},
});
});
it('should return error report when API is not running', async () => {
const result = await apiReportService.generateReport(false);
// Verify GraphQL client was not called
expect(mockInternalClientService.getClient).not.toHaveBeenCalled();
expect(mockClient.query).not.toHaveBeenCalled();
// Verify error report structure
expect(result).toMatchObject({
timestamp: expect.any(String),
connectionStatus: {
running: 'no',
},
system: {
name: 'Unknown',
version: 'Unknown',
machineId: 'REDACTED',
},
connect: {
installed: false,
reason: 'API is not running',
},
config: {
valid: null,
error: 'API is not running',
},
services: {
cloud: null,
minigraph: null,
allServices: [],
},
});
});
it('should handle connect plugin not available gracefully', async () => {
// Mock system and services queries to succeed, connect to fail
mockClient.query.mockImplementation(({ query }) => {
if (query === SYSTEM_REPORT_QUERY) {
return Promise.resolve({ data: mockSystemData });
} else if (query === CONNECT_STATUS_QUERY) {
return Promise.reject(new Error('Connect plugin not installed'));
} else if (query === SERVICES_QUERY) {
return Promise.resolve({ data: mockServicesData });
}
return Promise.reject(new Error('Unknown query'));
});
const result = await apiReportService.generateReport(true);
// Verify connect error was logged
expect(mockLogService.debug).toHaveBeenCalledWith(
expect.stringContaining('Connect plugin not available')
);
// Verify connect shows as not installed
expect(result.connect).toEqual({
installed: false,
reason: 'Connect plugin not installed or not available',
});
// Verify other data is still present
expect(result.system.name).toBe('Test Server');
expect(result.services.cloud).toBeTruthy();
});
it('should handle services query failure gracefully', async () => {
// Mock system and connect queries to succeed, services to fail
mockClient.query.mockImplementation(({ query }) => {
if (query === SYSTEM_REPORT_QUERY) {
return Promise.resolve({ data: mockSystemData });
} else if (query === CONNECT_STATUS_QUERY) {
return Promise.resolve({ data: mockConnectData });
} else if (query === SERVICES_QUERY) {
return Promise.reject(new Error('Services query failed'));
}
return Promise.reject(new Error('Unknown query'));
});
const result = await apiReportService.generateReport(true);
// Verify services error was logged
expect(mockLogService.debug).toHaveBeenCalledWith(
expect.stringContaining('Error querying services')
);
// Verify services shows empty
expect(result.services).toEqual({
cloud: null,
minigraph: null,
allServices: [],
});
// Verify other data is still present
expect(result.system.name).toBe('Test Server');
expect(result.connect.installed).toBe(true);
});
it('should handle missing server name gracefully', async () => {
const mockSystemDataWithoutServer = {
...mockSystemData,
server: null,
};
mockClient.query.mockImplementation(({ query }) => {
if (query === SYSTEM_REPORT_QUERY) {
return Promise.resolve({ data: mockSystemDataWithoutServer });
} else if (query === CONNECT_STATUS_QUERY) {
return Promise.resolve({ data: mockConnectData });
} else if (query === SERVICES_QUERY) {
return Promise.resolve({ data: mockServicesData });
}
return Promise.reject(new Error('Unknown query'));
});
const result = await apiReportService.generateReport(true);
expect(result.system.name).toBe('Unknown');
});
it('should handle services without uptime timestamps', async () => {
const mockServicesDataNoUptime = {
services: [
{
id: 'service-test',
name: 'test-service',
online: true,
uptime: null,
version: '1.0.0',
},
],
};
mockClient.query.mockImplementation(({ query }) => {
if (query === SYSTEM_REPORT_QUERY) {
return Promise.resolve({ data: mockSystemData });
} else if (query === CONNECT_STATUS_QUERY) {
return Promise.resolve({ data: mockConnectData });
} else if (query === SERVICES_QUERY) {
return Promise.resolve({ data: mockServicesDataNoUptime });
}
return Promise.reject(new Error('Unknown query'));
});
const result = await apiReportService.generateReport(true);
expect(result.services.allServices[0]).toMatchObject({
name: 'test-service',
online: true,
version: '1.0.0',
uptime: null,
});
});
it('should always redact sensitive information', async () => {
mockClient.query.mockImplementation(({ query }) => {
if (query === SYSTEM_REPORT_QUERY) {
return Promise.resolve({ data: mockSystemData });
} else if (query === CONNECT_STATUS_QUERY) {
return Promise.resolve({ data: mockConnectData });
} else if (query === SERVICES_QUERY) {
return Promise.resolve({ data: mockServicesData });
}
return Promise.reject(new Error('Unknown query'));
});
const result = await apiReportService.generateReport(true);
// Verify all sensitive fields are redacted
expect(result.system.machineId).toBe('REDACTED');
});
it('should handle connect with error gracefully', async () => {
const mockConnectDataWithError = {
connect: {
id: 'connect',
dynamicRemoteAccess: {
enabledType: 'STATIC',
runningType: 'DISABLED',
error: 'Port forwarding failed',
},
},
};
mockClient.query.mockImplementation(({ query }) => {
if (query === SYSTEM_REPORT_QUERY) {
return Promise.resolve({ data: mockSystemData });
} else if (query === CONNECT_STATUS_QUERY) {
return Promise.resolve({ data: mockConnectDataWithError });
} else if (query === SERVICES_QUERY) {
return Promise.resolve({ data: mockServicesData });
}
return Promise.reject(new Error('Unknown query'));
});
const result = await apiReportService.generateReport(true);
expect(result.connect).toMatchObject({
installed: true,
dynamicRemoteAccess: {
enabledType: 'STATIC',
runningType: 'DISABLED',
error: 'Port forwarding failed',
},
});
});
});
});

View File

@@ -0,0 +1,201 @@
import { Test, TestingModule } from '@nestjs/testing';
import { access, readFile, unlink, writeFile } from 'fs/promises';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { DeveloperToolsService } from '@app/unraid-api/cli/developer/developer-tools.service.js';
import { CliInternalClientService } from '@app/unraid-api/cli/internal-client.service.js';
import { LogService } from '@app/unraid-api/cli/log.service.js';
import { RestartCommand } from '@app/unraid-api/cli/restart.command.js';
vi.mock('fs/promises');
describe('DeveloperToolsService', () => {
let module: TestingModule;
let service: DeveloperToolsService;
let logService: LogService;
let restartCommand: RestartCommand;
let internalClient: CliInternalClientService;
const mockClient = {
mutate: vi.fn(),
};
beforeEach(async () => {
vi.clearAllMocks();
module = await Test.createTestingModule({
providers: [
DeveloperToolsService,
{
provide: LogService,
useValue: {
info: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
},
},
{
provide: RestartCommand,
useValue: {
run: vi.fn(),
},
},
{
provide: CliInternalClientService,
useValue: {
getClient: vi.fn().mockResolvedValue(mockClient),
},
},
],
}).compile();
service = module.get<DeveloperToolsService>(DeveloperToolsService);
logService = module.get<LogService>(LogService);
restartCommand = module.get<RestartCommand>(RestartCommand);
internalClient = module.get<CliInternalClientService>(CliInternalClientService);
});
describe('setSandboxMode', () => {
it('should enable sandbox mode and restart when required', async () => {
mockClient.mutate.mockResolvedValue({
data: {
updateSettings: {
restartRequired: true,
},
},
});
await service.setSandboxMode(true);
expect(mockClient.mutate).toHaveBeenCalledWith({
mutation: expect.any(Object),
variables: {
input: {
api: {
sandbox: true,
},
},
},
});
expect(logService.info).toHaveBeenCalledWith('Enabling sandbox mode - restarting API...');
expect(restartCommand.run).toHaveBeenCalled();
});
it('should disable sandbox mode without restart', async () => {
mockClient.mutate.mockResolvedValue({
data: {
updateSettings: {
restartRequired: false,
},
},
});
await service.setSandboxMode(false);
expect(logService.info).toHaveBeenCalledWith('Sandbox mode disabled successfully.');
expect(restartCommand.run).not.toHaveBeenCalled();
});
});
describe('enableModalTest', () => {
it('should create modal test page file', async () => {
vi.mocked(access).mockResolvedValue(undefined);
vi.mocked(writeFile).mockResolvedValue(undefined);
vi.mocked(readFile).mockResolvedValue('<html><body></body></html>');
await service.enableModalTest();
expect(access).toHaveBeenCalledWith('/usr/local/emhttp/plugins/dynamix.my.servers');
expect(writeFile).toHaveBeenCalledWith(
'/usr/local/emhttp/plugins/dynamix.my.servers/DevModalTest.page',
expect.stringContaining('unraid-dev-modal-test')
);
expect(logService.info).toHaveBeenCalledWith('✓ Modal test tool ENABLED');
expect(logService.info).toHaveBeenCalledWith(
'\nAccess the tool at: Menu > UNRAID-OS > Dev Modal Test'
);
});
it('should throw error if directory does not exist', async () => {
vi.mocked(access).mockRejectedValue(new Error('ENOENT'));
await expect(service.enableModalTest()).rejects.toThrow(
'Directory does not exist: /usr/local/emhttp/plugins/dynamix.my.servers'
);
});
});
describe('disableModalTest', () => {
it('should remove modal test page file', async () => {
vi.mocked(access).mockResolvedValue(undefined);
vi.mocked(unlink).mockResolvedValue(undefined);
await service.disableModalTest();
expect(unlink).toHaveBeenCalledWith(
'/usr/local/emhttp/plugins/dynamix.my.servers/DevModalTest.page'
);
expect(logService.info).toHaveBeenCalledWith('✓ Modal test tool DISABLED');
});
it('should handle file not existing', async () => {
vi.mocked(access).mockRejectedValue(new Error('ENOENT'));
await service.disableModalTest();
expect(unlink).not.toHaveBeenCalled();
expect(logService.info).toHaveBeenCalledWith('Modal test tool is already disabled.');
});
});
describe('isModalTestEnabled', () => {
it('should return true if file exists', async () => {
vi.mocked(access).mockResolvedValue(undefined);
const result = await service.isModalTestEnabled();
expect(result).toBe(true);
});
it('should return false if file does not exist', async () => {
vi.mocked(access).mockRejectedValue(new Error('ENOENT'));
const result = await service.isModalTestEnabled();
expect(result).toBe(false);
});
});
describe('getModalTestStatus', () => {
it('should return enabled status', async () => {
vi.mocked(access).mockResolvedValue(undefined);
const result = await service.getModalTestStatus();
expect(result).toEqual({
enabled: true,
});
});
it('should return disabled status', async () => {
vi.mocked(access).mockRejectedValue(new Error('ENOENT'));
const result = await service.getModalTestStatus();
expect(result).toEqual({
enabled: false,
});
});
});
describe('getModalTestingGuide', () => {
it('should return modal testing guide', () => {
const guide = service.getModalTestingGuide();
expect(guide).toBeInstanceOf(Array);
expect(guide[0]).toBe('Modal Testing Guide');
expect(guide).toContainEqual(' - Show/hide the Welcome Modal');
});
});
});

View File

@@ -0,0 +1,119 @@
import { Test, TestingModule } from '@nestjs/testing';
import { InquirerService } from 'nest-commander';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { DeveloperToolsService } from '@app/unraid-api/cli/developer/developer-tools.service.js';
import { DeveloperCommand } from '@app/unraid-api/cli/developer/developer.command.js';
import { LogService } from '@app/unraid-api/cli/log.service.js';
describe('DeveloperCommand', () => {
let module: TestingModule;
let command: DeveloperCommand;
let developerToolsService: DeveloperToolsService;
let logService: LogService;
let inquirerService: InquirerService;
beforeEach(async () => {
module = await Test.createTestingModule({
providers: [
DeveloperCommand,
{
provide: DeveloperToolsService,
useValue: {
setSandboxMode: vi.fn(),
enableModalTest: vi.fn(),
disableModalTest: vi.fn(),
getModalTestStatus: vi.fn().mockResolvedValue({ enabled: false }),
getModalTestingGuide: vi.fn().mockReturnValue(['test guide']),
},
},
{
provide: LogService,
useValue: {
info: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
},
},
{
provide: InquirerService,
useValue: {
prompt: vi.fn(),
},
},
],
}).compile();
command = module.get<DeveloperCommand>(DeveloperCommand);
developerToolsService = module.get<DeveloperToolsService>(DeveloperToolsService);
logService = module.get<LogService>(LogService);
inquirerService = module.get<InquirerService>(InquirerService);
});
it('should handle sandbox option directly', async () => {
await command.run([], { sandbox: true });
expect(developerToolsService.setSandboxMode).toHaveBeenCalledWith(true);
});
it('should handle enable-modal option directly', async () => {
await command.run([], { 'enable-modal': true });
expect(developerToolsService.enableModalTest).toHaveBeenCalled();
expect(logService.info).toHaveBeenCalledWith('test guide');
});
it('should handle disable-modal option directly', async () => {
await command.run([], { 'disable-modal': true });
expect(developerToolsService.disableModalTest).toHaveBeenCalled();
});
it('should show modal test status', async () => {
vi.mocked(inquirerService.prompt).mockResolvedValue({
tool: 'modal-test',
modalAction: 'status',
});
await command.run([], {});
expect(developerToolsService.getModalTestStatus).toHaveBeenCalled();
expect(logService.info).toHaveBeenCalledWith('Modal Test Tool Status');
});
it('should handle sandbox selection in interactive mode', async () => {
vi.mocked(inquirerService.prompt).mockResolvedValue({
tool: 'sandbox',
sandboxEnabled: true,
});
await command.run([], {});
expect(developerToolsService.setSandboxMode).toHaveBeenCalledWith(true);
});
it('should handle modal test enable in interactive mode', async () => {
vi.mocked(inquirerService.prompt).mockResolvedValue({
tool: 'modal-test',
modalAction: 'enable',
});
await command.run([], {});
expect(developerToolsService.enableModalTest).toHaveBeenCalled();
});
it('should handle errors gracefully', async () => {
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit called');
});
vi.mocked(developerToolsService.setSandboxMode).mockRejectedValue(new Error('Test error'));
await expect(command.run([], { sandbox: true })).rejects.toThrow('process.exit called');
expect(mockExit).toHaveBeenCalledWith(1);
mockExit.mockRestore();
});
});

View File

@@ -0,0 +1,86 @@
import { Test } from '@nestjs/testing';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { CliInternalClientService } from '@app/unraid-api/cli/internal-client.service.js';
import { LogService } from '@app/unraid-api/cli/log.service.js';
import { ListSSOUserCommand } from '@app/unraid-api/cli/sso/list-sso-user.command.js';
// Mock services
const mockInternalClient = {
getClient: vi.fn(),
};
const mockLogger = {
info: vi.fn(),
error: vi.fn(),
};
describe('ListSSOUserCommand', () => {
let command: ListSSOUserCommand;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
ListSSOUserCommand,
{ provide: CliInternalClientService, useValue: mockInternalClient },
{ provide: LogService, useValue: mockLogger },
],
}).compile();
command = module.get<ListSSOUserCommand>(ListSSOUserCommand);
// Clear mocks
vi.clearAllMocks();
});
it('should list all SSO users', async () => {
const mockClient = {
query: vi.fn().mockResolvedValue({
data: {
settings: {
api: {
ssoSubIds: ['user-1', 'user-2', 'user-3'],
},
},
},
}),
};
mockInternalClient.getClient.mockResolvedValue(mockClient);
await command.run([]);
expect(mockClient.query).toHaveBeenCalledWith({
query: expect.anything(),
});
expect(mockLogger.info).toHaveBeenCalledWith('user-1\nuser-2\nuser-3');
});
it('should display message when no users found', async () => {
const mockClient = {
query: vi.fn().mockResolvedValue({
data: {
settings: {
api: {
ssoSubIds: [],
},
},
},
}),
};
mockInternalClient.getClient.mockResolvedValue(mockClient);
await command.run([]);
expect(mockClient.query).toHaveBeenCalled();
expect(mockLogger.info).toHaveBeenCalledWith('No SSO users found');
});
it('should handle errors gracefully', async () => {
mockInternalClient.getClient.mockRejectedValue(new Error('Connection failed'));
await expect(command.run([])).rejects.toThrow('Connection failed');
});
});

View File

@@ -0,0 +1,274 @@
import { Test } from '@nestjs/testing';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { CliInternalClientService } from '@app/unraid-api/cli/internal-client.service.js';
import { LogService } from '@app/unraid-api/cli/log.service.js';
import {
InstallPluginCommand,
ListPluginCommand,
RemovePluginCommand,
} from '@app/unraid-api/cli/plugins/plugin.command.js';
import { RestartCommand } from '@app/unraid-api/cli/restart.command.js';
// Mock services
const mockInternalClient = {
getClient: vi.fn(),
};
const mockLogger = {
log: vi.fn(),
info: vi.fn(),
error: vi.fn(),
};
const mockRestartCommand = {
run: vi.fn(),
};
describe('Plugin Commands', () => {
beforeEach(() => {
// Clear mocks before each test
vi.clearAllMocks();
});
describe('InstallPluginCommand', () => {
let command: InstallPluginCommand;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
InstallPluginCommand,
{ provide: CliInternalClientService, useValue: mockInternalClient },
{ provide: LogService, useValue: mockLogger },
{ provide: RestartCommand, useValue: mockRestartCommand },
],
}).compile();
command = module.get<InstallPluginCommand>(InstallPluginCommand);
});
it('should install a plugin successfully', async () => {
const mockClient = {
mutate: vi.fn().mockResolvedValue({
data: {
addPlugin: false, // No manual restart required
},
}),
};
mockInternalClient.getClient.mockResolvedValue(mockClient);
await command.run(['@unraid/plugin-example'], { bundled: false, restart: true });
expect(mockClient.mutate).toHaveBeenCalledWith({
mutation: expect.anything(),
variables: {
input: {
names: ['@unraid/plugin-example'],
bundled: false,
restart: true,
},
},
});
expect(mockLogger.log).toHaveBeenCalledWith('Added plugin @unraid/plugin-example');
expect(mockRestartCommand.run).not.toHaveBeenCalled(); // Because addPlugin returned false
});
it('should handle bundled plugin installation', async () => {
const mockClient = {
mutate: vi.fn().mockResolvedValue({
data: {
addPlugin: true, // Manual restart required
},
}),
};
mockInternalClient.getClient.mockResolvedValue(mockClient);
await command.run(['@unraid/bundled-plugin'], { bundled: true, restart: true });
expect(mockClient.mutate).toHaveBeenCalledWith({
mutation: expect.anything(),
variables: {
input: {
names: ['@unraid/bundled-plugin'],
bundled: true,
restart: true,
},
},
});
expect(mockLogger.log).toHaveBeenCalledWith('Added bundled plugin @unraid/bundled-plugin');
expect(mockRestartCommand.run).toHaveBeenCalled(); // Because addPlugin returned true
});
it('should not restart when restart option is false', async () => {
const mockClient = {
mutate: vi.fn().mockResolvedValue({
data: {
addPlugin: true,
},
}),
};
mockInternalClient.getClient.mockResolvedValue(mockClient);
await command.run(['@unraid/plugin'], { bundled: false, restart: false });
expect(mockRestartCommand.run).not.toHaveBeenCalled();
});
it('should handle errors', async () => {
mockInternalClient.getClient.mockRejectedValue(new Error('Connection failed'));
await command.run(['@unraid/plugin'], { bundled: false, restart: true });
expect(mockLogger.error).toHaveBeenCalledWith('Failed to add plugin:', expect.any(Error));
expect(process.exitCode).toBe(1);
});
it('should error when no package name provided', async () => {
await command.run([], { bundled: false, restart: true });
expect(mockLogger.error).toHaveBeenCalledWith('Package name is required.');
expect(process.exitCode).toBe(1);
});
});
describe('RemovePluginCommand', () => {
let command: RemovePluginCommand;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
RemovePluginCommand,
{ provide: CliInternalClientService, useValue: mockInternalClient },
{ provide: LogService, useValue: mockLogger },
{ provide: RestartCommand, useValue: mockRestartCommand },
],
}).compile();
command = module.get<RemovePluginCommand>(RemovePluginCommand);
});
it('should remove a plugin successfully', async () => {
const mockClient = {
mutate: vi.fn().mockResolvedValue({
data: {
removePlugin: false, // No manual restart required
},
}),
};
mockInternalClient.getClient.mockResolvedValue(mockClient);
await command.run(['@unraid/plugin-example'], { bundled: false, restart: true });
expect(mockClient.mutate).toHaveBeenCalledWith({
mutation: expect.anything(),
variables: {
input: {
names: ['@unraid/plugin-example'],
bundled: false,
restart: true,
},
},
});
expect(mockLogger.log).toHaveBeenCalledWith('Removed plugin @unraid/plugin-example');
expect(mockRestartCommand.run).not.toHaveBeenCalled();
});
it('should handle removing bundled plugins', async () => {
const mockClient = {
mutate: vi.fn().mockResolvedValue({
data: {
removePlugin: true, // Manual restart required
},
}),
};
mockInternalClient.getClient.mockResolvedValue(mockClient);
await command.run(['@unraid/bundled-plugin'], { bundled: true, restart: true });
expect(mockLogger.log).toHaveBeenCalledWith('Removed bundled plugin @unraid/bundled-plugin');
expect(mockRestartCommand.run).toHaveBeenCalled();
});
});
describe('ListPluginCommand', () => {
let command: ListPluginCommand;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
ListPluginCommand,
{ provide: CliInternalClientService, useValue: mockInternalClient },
{ provide: LogService, useValue: mockLogger },
],
}).compile();
command = module.get<ListPluginCommand>(ListPluginCommand);
});
it('should list installed plugins', async () => {
const mockClient = {
query: vi.fn().mockResolvedValue({
data: {
plugins: [
{
name: '@unraid/plugin-1',
version: '1.0.0',
hasApiModule: true,
hasCliModule: false,
},
{
name: '@unraid/plugin-2',
version: '2.0.0',
hasApiModule: true,
hasCliModule: true,
},
],
},
}),
};
mockInternalClient.getClient.mockResolvedValue(mockClient);
await command.run();
expect(mockClient.query).toHaveBeenCalledWith({
query: expect.anything(),
});
expect(mockLogger.log).toHaveBeenCalledWith('Installed plugins:\n');
expect(mockLogger.log).toHaveBeenCalledWith('☑️ @unraid/plugin-1@1.0.0 [API]');
expect(mockLogger.log).toHaveBeenCalledWith('☑️ @unraid/plugin-2@2.0.0 [API, CLI]');
expect(mockLogger.log).toHaveBeenCalledWith();
});
it('should handle no plugins installed', async () => {
const mockClient = {
query: vi.fn().mockResolvedValue({
data: {
plugins: [],
},
}),
};
mockInternalClient.getClient.mockResolvedValue(mockClient);
await command.run();
expect(mockLogger.log).toHaveBeenCalledWith('No plugins installed.');
});
it('should handle errors', async () => {
mockInternalClient.getClient.mockRejectedValue(new Error('Connection failed'));
await command.run();
expect(mockLogger.error).toHaveBeenCalledWith('Failed to list plugins:', expect.any(Error));
expect(process.exitCode).toBe(1);
});
});
});

View File

@@ -0,0 +1,186 @@
import { Test } from '@nestjs/testing';
import { InquirerService } from 'nest-commander';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { CliInternalClientService } from '@app/unraid-api/cli/internal-client.service.js';
import { LogService } from '@app/unraid-api/cli/log.service.js';
import { RestartCommand } from '@app/unraid-api/cli/restart.command.js';
import { RemoveSSOUserCommand } from '@app/unraid-api/cli/sso/remove-sso-user.command.js';
// Mock services
const mockInternalClient = {
getClient: vi.fn(),
};
const mockLogger = {
info: vi.fn(),
error: vi.fn(),
};
const mockRestartCommand = {
run: vi.fn(),
};
const mockInquirerService = {
prompt: vi.fn(),
};
describe('RemoveSSOUserCommand', () => {
let command: RemoveSSOUserCommand;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
RemoveSSOUserCommand,
{ provide: CliInternalClientService, useValue: mockInternalClient },
{ provide: LogService, useValue: mockLogger },
{ provide: RestartCommand, useValue: mockRestartCommand },
{ provide: InquirerService, useValue: mockInquirerService },
],
}).compile();
command = module.get<RemoveSSOUserCommand>(RemoveSSOUserCommand);
// Clear mocks
vi.clearAllMocks();
});
it('should remove a specific SSO user successfully', async () => {
const mockClient = {
query: vi.fn().mockResolvedValue({
data: {
settings: {
api: {
ssoSubIds: ['user-1', 'user-2', 'user-3'],
},
},
},
}),
mutate: vi.fn().mockResolvedValue({
data: {
updateSettings: {
restartRequired: true,
values: {},
},
},
}),
};
mockInternalClient.getClient.mockResolvedValue(mockClient);
mockInquirerService.prompt.mockResolvedValue({
username: 'user-2',
});
await command.run([]);
expect(mockClient.query).toHaveBeenCalled();
expect(mockClient.mutate).toHaveBeenCalledWith({
mutation: expect.anything(),
variables: {
input: {
api: {
ssoSubIds: ['user-1', 'user-3'],
},
},
},
});
expect(mockLogger.info).toHaveBeenCalledWith('User removed: user-2');
expect(mockLogger.info).toHaveBeenCalledWith('Restarting the API');
expect(mockRestartCommand.run).toHaveBeenCalled();
});
it('should remove all SSO users when "all" is selected', async () => {
const mockClient = {
query: vi.fn().mockResolvedValue({
data: {
settings: {
api: {
ssoSubIds: ['user-1', 'user-2', 'user-3'],
},
},
},
}),
mutate: vi.fn().mockResolvedValue({
data: {
updateSettings: {
restartRequired: true,
values: {},
},
},
}),
};
mockInternalClient.getClient.mockResolvedValue(mockClient);
mockInquirerService.prompt.mockResolvedValue({
username: 'all',
});
await command.run([]);
expect(mockClient.query).toHaveBeenCalled();
expect(mockClient.mutate).toHaveBeenCalledWith({
mutation: expect.anything(),
variables: {
input: {
api: {
ssoSubIds: [],
},
},
},
});
expect(mockLogger.info).toHaveBeenCalledWith('All users removed from SSO');
expect(mockRestartCommand.run).toHaveBeenCalled();
});
it('should not remove user if user does not exist', async () => {
const mockClient = {
query: vi.fn().mockResolvedValue({
data: {
settings: {
api: {
ssoSubIds: ['user-1', 'user-3'],
},
},
},
}),
mutate: vi.fn(),
};
mockInternalClient.getClient.mockResolvedValue(mockClient);
mockInquirerService.prompt.mockResolvedValue({
username: 'user-2',
});
await command.run([]);
expect(mockClient.query).toHaveBeenCalled();
expect(mockClient.mutate).not.toHaveBeenCalled();
expect(mockLogger.error).toHaveBeenCalledWith('User user-2 not found in SSO users');
expect(mockRestartCommand.run).not.toHaveBeenCalled();
});
it('should exit when no SSO users are found', async () => {
const processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit');
});
const error = new Error('No SSO Users Found');
(error as any).name = 'NoSSOUsersFoundError';
mockInquirerService.prompt.mockRejectedValue(error);
try {
await command.run([]);
} catch (error) {
// Expected to throw due to process.exit
}
expect(mockLogger.error).toHaveBeenCalledWith(
'Failed to fetch SSO users: %s',
'No SSO Users Found'
);
expect(processExitSpy).toHaveBeenCalledWith(1);
processExitSpy.mockRestore();
});
});

View File

@@ -0,0 +1,153 @@
import { Test } from '@nestjs/testing';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ApiReportService } from '@app/unraid-api/cli/api-report.service.js';
import { LogService } from '@app/unraid-api/cli/log.service.js';
import { ReportCommand } from '@app/unraid-api/cli/report.command.js';
// Mock log service
const mockLogService = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
clear: vi.fn(),
};
// Mock ApiReportService
const mockApiReportService = {
generateReport: vi.fn(),
};
// Mock PM2 check
const mockIsUnraidApiRunning = vi.fn().mockResolvedValue(true);
vi.mock('@app/core/utils/pm2/unraid-api-running.js', () => ({
isUnraidApiRunning: () => mockIsUnraidApiRunning(),
}));
describe('ReportCommand', () => {
let reportCommand: ReportCommand;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
ReportCommand,
{ provide: LogService, useValue: mockLogService },
{ provide: ApiReportService, useValue: mockApiReportService },
],
}).compile();
reportCommand = module.get<ReportCommand>(ReportCommand);
// Clear mocks
vi.clearAllMocks();
// Reset PM2 mock to default
mockIsUnraidApiRunning.mockResolvedValue(true);
});
describe('report', () => {
it('should generate report using ApiReportService when API is running', async () => {
const mockReport = {
timestamp: '2023-01-01T00:00:00.000Z',
connectionStatus: {
running: 'yes' as const,
},
system: {
id: 'test-uuid',
name: 'Test Server',
version: '6.12.0',
machineId: 'REDACTED',
manufacturer: 'Test Manufacturer',
model: 'Test Model',
},
connect: {
installed: true,
dynamicRemoteAccess: {
enabledType: 'STATIC',
runningType: 'STATIC',
error: null,
},
},
config: {
valid: true,
error: null,
},
services: {
cloud: { name: 'cloud', online: true },
minigraph: { name: 'minigraph', online: false },
allServices: [],
},
remote: {
apikey: 'REDACTED',
localApiKey: 'REDACTED',
accesstoken: 'REDACTED',
idtoken: 'REDACTED',
refreshtoken: 'REDACTED',
ssoSubIds: 'REDACTED',
allowedOrigins: 'REDACTED',
email: 'REDACTED',
},
};
mockApiReportService.generateReport.mockResolvedValue(mockReport);
await reportCommand.report();
// Verify ApiReportService was called with correct parameter
expect(mockApiReportService.generateReport).toHaveBeenCalledWith(true);
// Verify report was logged
expect(mockLogService.clear).toHaveBeenCalled();
expect(mockLogService.info).toHaveBeenCalledWith(JSON.stringify(mockReport, null, 2));
});
it('should handle API not running gracefully', async () => {
mockIsUnraidApiRunning.mockResolvedValue(false);
await reportCommand.report();
// Verify ApiReportService was not called
expect(mockApiReportService.generateReport).not.toHaveBeenCalled();
// Verify warning was logged
expect(mockLogService.warn).toHaveBeenCalledWith(
expect.stringContaining('API is not running')
);
});
it('should handle ApiReportService errors gracefully', async () => {
const error = new Error('Report generation failed');
mockApiReportService.generateReport.mockRejectedValue(error);
await reportCommand.report();
// Verify error was logged
expect(mockLogService.debug).toHaveBeenCalledWith(
expect.stringContaining('Error generating report via GraphQL')
);
expect(mockLogService.warn).toHaveBeenCalledWith(
expect.stringContaining('Failed to generate system report')
);
});
it('should pass correct apiRunning parameter to ApiReportService', async () => {
const mockReport = { timestamp: '2023-01-01T00:00:00.000Z' };
mockApiReportService.generateReport.mockResolvedValue(mockReport);
// Test with API running
await reportCommand.report();
expect(mockApiReportService.generateReport).toHaveBeenCalledWith(true);
// Reset mocks
vi.clearAllMocks();
// Test with API running but PM2 check returns true
mockIsUnraidApiRunning.mockResolvedValue(true);
await reportCommand.report();
expect(mockApiReportService.generateReport).toHaveBeenCalledWith(true);
});
});
});

View File

@@ -0,0 +1,44 @@
import { Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common';
import type { ApiKeyService } from '@unraid/shared/services/api-key.js';
import { Role } from '@unraid/shared/graphql.model.js';
import { API_KEY_SERVICE_TOKEN } from '@unraid/shared/tokens.js';
/**
* Service that creates and manages the admin API key used by CLI commands.
* Uses the standard API key storage location via helper methods in ApiKeyService.
*/
@Injectable()
export class AdminKeyService implements OnModuleInit {
private readonly logger = new Logger(AdminKeyService.name);
private static readonly ADMIN_KEY_NAME = 'CliInternal';
private static readonly ADMIN_KEY_DESCRIPTION =
'Internal admin API key used by CLI commands for system operations';
constructor(
@Inject(API_KEY_SERVICE_TOKEN)
private readonly apiKeyService: ApiKeyService
) {}
async onModuleInit() {
try {
await this.getOrCreateLocalAdminKey();
this.logger.log('Admin API key initialized successfully');
} catch (error) {
this.logger.error('Failed to initialize admin API key:', error);
}
}
/**
* Gets or creates a local admin API key for CLI operations.
* Uses the standard API key storage location.
*/
public async getOrCreateLocalAdminKey(): Promise<string> {
return this.apiKeyService.ensureKey({
name: AdminKeyService.ADMIN_KEY_NAME,
description: AdminKeyService.ADMIN_KEY_DESCRIPTION,
roles: [Role.ADMIN], // Full admin privileges for CLI operations
legacyNames: ['CLI', 'Internal', 'CliAdmin'], // Clean up old keys
});
}
}

View File

@@ -0,0 +1,198 @@
import { Injectable } from '@nestjs/common';
import type { ConnectStatusQuery, SystemReportQuery } from '@app/unraid-api/cli/generated/graphql.js';
import { CliInternalClientService } from '@app/unraid-api/cli/internal-client.service.js';
import { LogService } from '@app/unraid-api/cli/log.service.js';
import {
CONNECT_STATUS_QUERY,
SERVICES_QUERY,
SYSTEM_REPORT_QUERY,
} from '@app/unraid-api/cli/queries/system-report.query.js';
export interface ServiceInfo {
id?: string | null;
name?: string | null;
online?: boolean | null;
version?: string | null;
uptime?: {
timestamp?: string | null;
} | null;
}
export interface ApiReportData {
timestamp: string;
connectionStatus: {
running: 'yes' | 'no';
};
system: {
id?: string | null;
name: string;
version: string;
machineId: string;
manufacturer?: string | null;
model?: string | null;
};
connect: {
installed: boolean;
dynamicRemoteAccess?: {
enabledType: string;
runningType: string;
error?: string | null;
};
reason?: string;
};
config: {
valid?: boolean | null;
error?: string | null;
};
services: {
cloud: ServiceInfo | null;
minigraph: ServiceInfo | null;
allServices: Array<{
name?: string | null;
online?: boolean | null;
version?: string | null;
uptime?: string | null;
}>;
};
}
@Injectable()
export class ApiReportService {
constructor(
private readonly internalClient: CliInternalClientService,
private readonly logger: LogService
) {}
private createApiReportData(params: {
apiRunning: boolean;
systemData?: SystemReportQuery;
connectData?: ConnectStatusQuery['connect'] | null;
servicesData?: ServiceInfo[];
errorReason?: string;
}): ApiReportData {
const { apiRunning, systemData, connectData, servicesData = [], errorReason } = params;
return {
timestamp: new Date().toISOString(),
connectionStatus: {
running: apiRunning ? 'yes' : 'no',
},
system: systemData
? {
id: systemData.info.system.uuid,
name: systemData.server?.name || 'Unknown',
version: systemData.info.versions.unraid || 'Unknown',
machineId: 'REDACTED',
manufacturer: systemData.info.system.manufacturer,
model: systemData.info.system.model,
}
: {
name: 'Unknown',
version: 'Unknown',
machineId: 'REDACTED',
},
connect: connectData
? {
installed: true,
dynamicRemoteAccess: {
enabledType: connectData.dynamicRemoteAccess.enabledType,
runningType: connectData.dynamicRemoteAccess.runningType,
error: connectData.dynamicRemoteAccess.error || null,
},
}
: {
installed: false,
reason: errorReason || 'Connect plugin not installed or not available',
},
config: systemData
? {
valid: systemData.config.valid,
error: systemData.config.error || null,
}
: {
valid: null,
error: errorReason || 'Unable to retrieve config',
},
services: {
cloud: servicesData.find((s) => s.name === 'cloud') || null,
minigraph: servicesData.find((s) => s.name === 'minigraph') || null,
allServices: servicesData.map((s) => ({
name: s.name,
online: s.online,
version: s.version,
uptime: s.uptime?.timestamp || null,
})),
},
};
}
async generateReport(apiRunning = true): Promise<ApiReportData> {
if (!apiRunning) {
return this.createApiReportData({
apiRunning: false,
errorReason: 'API is not running',
});
}
const client = await this.internalClient.getClient();
// Query system data
let systemResult: { data: SystemReportQuery } | null = null;
try {
systemResult = await Promise.race([
client.query({
query: SYSTEM_REPORT_QUERY,
}),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Query timeout after 10 seconds')), 10000)
),
]);
} catch (error) {
this.logger.error('Error querying system data: ' + error);
return this.createApiReportData({
apiRunning,
errorReason: 'System query failed',
});
}
// Try to query connect status
let connectData: ConnectStatusQuery['connect'] | null = null;
try {
const connectResult = await Promise.race([
client.query({
query: CONNECT_STATUS_QUERY,
}),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Connect query timeout after 10 seconds')), 10000)
),
]);
connectData = connectResult.data.connect;
} catch (error) {
this.logger.debug('Connect plugin not available: ' + error);
}
// Query services
let servicesData: ServiceInfo[] = [];
try {
const servicesResult = await Promise.race([
client.query({
query: SERVICES_QUERY,
}),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Services query timeout after 10 seconds')), 10000)
),
]);
servicesData = servicesResult.data.services || [];
} catch (error) {
this.logger.debug('Error querying services: ' + error);
}
return this.createApiReportData({
apiRunning,
systemData: systemResult.data,
connectData,
servicesData,
});
}
}

View File

@@ -0,0 +1,39 @@
import { Module } from '@nestjs/common';
import { DependencyService } from '@app/unraid-api/app/dependency.service.js';
import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js';
import { SsoUserService } from '@app/unraid-api/auth/sso-user.service.js';
import { AdminKeyService } from '@app/unraid-api/cli/admin-key.service.js';
import { ApiReportService } from '@app/unraid-api/cli/api-report.service.js';
import { CliInternalClientService } from '@app/unraid-api/cli/internal-client.service.js';
import { LogService } from '@app/unraid-api/cli/log.service.js';
import { PM2Service } from '@app/unraid-api/cli/pm2.service.js';
import { ApiConfigModule } from '@app/unraid-api/config/api-config.module.js';
import { LegacyConfigModule } from '@app/unraid-api/config/legacy-config.module.js';
import { GlobalDepsModule } from '@app/unraid-api/plugin/global-deps.module.js';
import { PluginCliModule } from '@app/unraid-api/plugin/plugin.module.js';
import { UnraidFileModifierModule } from '@app/unraid-api/unraid-file-modifier/unraid-file-modifier.module.js';
// This module provides only the services from CliModule without the CLI commands
// This avoids dependency issues with InquirerService when used in other modules
@Module({
imports: [
LegacyConfigModule,
ApiConfigModule,
GlobalDepsModule,
PluginCliModule.register(),
UnraidFileModifierModule,
],
providers: [
LogService,
PM2Service,
ApiKeyService,
SsoUserService,
DependencyService,
AdminKeyService,
ApiReportService,
CliInternalClientService,
],
exports: [ApiReportService, LogService, ApiKeyService, SsoUserService, CliInternalClientService],
})
export class CliServicesModule {}

View File

@@ -3,12 +3,16 @@ import { Module } from '@nestjs/common';
import { DependencyService } from '@app/unraid-api/app/dependency.service.js';
import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js';
import { SsoUserService } from '@app/unraid-api/auth/sso-user.service.js';
import { AdminKeyService } from '@app/unraid-api/cli/admin-key.service.js';
import { ApiReportService } from '@app/unraid-api/cli/api-report.service.js';
import { AddApiKeyQuestionSet } from '@app/unraid-api/cli/apikey/add-api-key.questions.js';
import { ApiKeyCommand } from '@app/unraid-api/cli/apikey/api-key.command.js';
import { DeleteApiKeyQuestionSet } from '@app/unraid-api/cli/apikey/delete-api-key.questions.js';
import { ConfigCommand } from '@app/unraid-api/cli/config.command.js';
import { DeveloperToolsService } from '@app/unraid-api/cli/developer/developer-tools.service.js';
import { DeveloperCommand } from '@app/unraid-api/cli/developer/developer.command.js';
import { DeveloperQuestions } from '@app/unraid-api/cli/developer/developer.questions.js';
import { CliInternalClientService } from '@app/unraid-api/cli/internal-client.service.js';
import { LogService } from '@app/unraid-api/cli/log.service.js';
import { LogsCommand } from '@app/unraid-api/cli/logs.command.js';
import {
@@ -34,6 +38,7 @@ import { SwitchEnvCommand } from '@app/unraid-api/cli/switch-env.command.js';
import { VersionCommand } from '@app/unraid-api/cli/version.command.js';
import { ApiConfigModule } from '@app/unraid-api/config/api-config.module.js';
import { LegacyConfigModule } from '@app/unraid-api/config/legacy-config.module.js';
import { GlobalDepsModule } from '@app/unraid-api/plugin/global-deps.module.js';
import { PluginCliModule } from '@app/unraid-api/plugin/plugin.module.js';
const DEFAULT_COMMANDS = [
@@ -68,15 +73,20 @@ const DEFAULT_PROVIDERS = [
AddSSOUserQuestionSet,
RemoveSSOUserQuestionSet,
DeveloperQuestions,
DeveloperToolsService,
LogService,
PM2Service,
ApiKeyService,
SsoUserService,
DependencyService,
AdminKeyService,
ApiReportService,
CliInternalClientService,
] as const;
@Module({
imports: [LegacyConfigModule, ApiConfigModule, PluginCliModule.register()],
imports: [LegacyConfigModule, ApiConfigModule, GlobalDepsModule, PluginCliModule.register()],
providers: [...DEFAULT_COMMANDS, ...DEFAULT_PROVIDERS],
exports: [ApiReportService],
})
export class CliModule {}

Some files were not shown because too many files have changed in this diff Show More