From 1fe956da1028cd3f02b47e46a6440e0e34ac75f4 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Fri, 5 Jun 2026 17:02:14 -0600 Subject: [PATCH 1/7] ci: use node from 20 to 26 --- .github/workflows/build.yml | 2 +- .github/workflows/quality.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d5faef96..0ba335f3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -121,7 +121,7 @@ jobs: - name: Install Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: "20" + node-version: "26" - name: Install npm dependencies run: npm i diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index b4c601de..f1007962 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -8,7 +8,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 20 + node-version: 26 - name: Install npm packages run: npm install - name: Check Prettier From 725757da9a98142e5bc97465e7f0e8aedc6a8aa8 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Fri, 5 Jun 2026 17:02:54 -0600 Subject: [PATCH 2/7] feat: add build for pfSense Plus 26.03.1 --- .github/workflows/release.yml | 2 ++ README.md | 2 +- docs/INSTALL_AND_CONFIG.md | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d09b5a9c..19098db9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,6 +32,8 @@ jobs: PFSENSE_VERSION: "25.11.1" - FREEBSD_VERSION: FreeBSD-16.0-CURRENT PFSENSE_VERSION: "26.03" + - FREEBSD_VERSION: FreeBSD-16.0-CURRENT + PFSENSE_VERSION: "26.03.1" steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/README.md b/README.md index 8b902d1e..3687aa56 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ pkg-static add https://github.com/pfrest/pfSense-pkg-RESTAPI/releases/latest/dow Install on pfSense Plus: ```bash -pkg-static -C /dev/null add https://github.com/pfrest/pfSense-pkg-RESTAPI/releases/latest/download/pfSense-26.03-pkg-RESTAPI.pkg +pkg-static -C /dev/null add https://github.com/pfrest/pfSense-pkg-RESTAPI/releases/latest/download/pfSense-26.03.1-pkg-RESTAPI.pkg ``` > [!WARNING] diff --git a/docs/INSTALL_AND_CONFIG.md b/docs/INSTALL_AND_CONFIG.md index 63fadf30..0c21b0f2 100644 --- a/docs/INSTALL_AND_CONFIG.md +++ b/docs/INSTALL_AND_CONFIG.md @@ -17,6 +17,8 @@ run pfSense. It's recommended to follow Netgate's [minimum hardware requirements - pfSense CE 2.8.1 - pfSense Plus 25.11.1 - pfSense Plus 26.03 +- pfSense Plus 26.03.1 + !!! Warning Installation of the package on unsupported versions of pfSense may result in unexpected behavior and/or system instability. From 7e354c323425348183ab5434203df9bf575ab586 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Fri, 5 Jun 2026 17:06:40 -0600 Subject: [PATCH 3/7] docs: fix bad indents in filter docs --- docs/QUERIES_FILTERS_AND_SORTING.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/QUERIES_FILTERS_AND_SORTING.md b/docs/QUERIES_FILTERS_AND_SORTING.md index 5e85533d..3d7773bf 100644 --- a/docs/QUERIES_FILTERS_AND_SORTING.md +++ b/docs/QUERIES_FILTERS_AND_SORTING.md @@ -56,10 +56,11 @@ value array contains a given value for array fields. Search for objects whose field value is within a given array of values (when the filter value is an array), or search for objects whose field value is a substring of a given string (when the filter value is a string). + - Name: `in` - Examples: - - `https://pfsense.example.com/api/v2/examples?fieldname__in=example` - - `https://pfsense.example.com/api/v2/examples?fieldname__in[]=example1&fieldname__in[]=example2` + - `https://pfsense.example.com/api/v2/examples?fieldname__in=example` + - `https://pfsense.example.com/api/v2/examples?fieldname__in[]=example1&fieldname__in[]=example2` ### Less Than (lt) @@ -95,8 +96,8 @@ Search for objects whose field value matches a given format. - Name: `format` - Examples: - - `https://pfsense.example.com/api/v2/examples?fieldname__format=ipv4` - - `https://pfsense.example.com/api/v2/examples?fieldname__format=email` + - `https://pfsense.example.com/api/v2/examples?fieldname__format=ipv4` + - `https://pfsense.example.com/api/v2/examples?fieldname__format=email` #### Supported Formats From fbee00c442efd2296868e9c4c39f2e6d75c712e7 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Fri, 5 Jun 2026 17:46:23 -0600 Subject: [PATCH 4/7] fix(RESTAPIVersionReleasesCache): don't cache bad gh api responses #900 --- .../RESTAPI/Caches/RESTAPIVersionReleasesCache.inc | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Caches/RESTAPIVersionReleasesCache.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Caches/RESTAPIVersionReleasesCache.inc index 7eddd76a..7b956f29 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Caches/RESTAPIVersionReleasesCache.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Caches/RESTAPIVersionReleasesCache.inc @@ -32,7 +32,16 @@ class RESTAPIVersionReleasesCache extends Cache { ); $releases = json_decode($releases_json, true); - # Return the fetched releases if in a valid format, otherwise retain the existing cache - return is_array($releases) ? $releases : $this->read(); + # If we don't have an 'assets' key, the response is invalid. Log the response, keep the existing cache + if (!is_array($releases) or !array_key_exists('assets', $releases)) { + Cache::log( + level: LOG_ERR, + message: "Releases data is malformed, received $releases_json. Keeping existing cache.", + ); + return $this->read(); + } + + # Return the fetched releases + return $releases; } } From d8d4732b5a820a69f42e744a43844bebdf0defa0 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Fri, 5 Jun 2026 17:51:48 -0600 Subject: [PATCH 5/7] fix(WireGuardTunnel): ignore addresses if tunnel has a current iface assignment #902 --- .../local/pkg/RESTAPI/Models/WireGuardTunnel.inc | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/WireGuardTunnel.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/WireGuardTunnel.inc index fcf38ce6..7feb9b2a 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/WireGuardTunnel.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/WireGuardTunnel.inc @@ -145,6 +145,19 @@ class WireGuardTunnel extends Model { return wg_is_key_clamped($privatekey) ? $privatekey : wg_clamp_key($privatekey); } + /** + * Extends the default _update method to ensure addresses are removed if the tunnel has an interface assignment + */ + public function _update(): void + { + # Remove any existing addresses if this tunnel has an existing interface assignment + if (NetworkInterface::query(if: $this->name->value)->exists()) { + $this->addresses->value = null; + } + + parent::_update(); + } + /** * Obtains the next available WireGuard tunnel interface name. * @return string The next available WireGuard tunnel interface name (i.e. tun_wg0) From e81b30ce2f33dc2bcad962945969742a72ce4dce Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Fri, 5 Jun 2026 17:52:18 -0600 Subject: [PATCH 6/7] style: run prettier on changed files --- .../files/usr/local/pkg/RESTAPI/Models/WireGuardTunnel.inc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/WireGuardTunnel.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/WireGuardTunnel.inc index 7feb9b2a..17650ea5 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/WireGuardTunnel.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/WireGuardTunnel.inc @@ -148,8 +148,7 @@ class WireGuardTunnel extends Model { /** * Extends the default _update method to ensure addresses are removed if the tunnel has an interface assignment */ - public function _update(): void - { + public function _update(): void { # Remove any existing addresses if this tunnel has an existing interface assignment if (NetworkInterface::query(if: $this->name->value)->exists()) { $this->addresses->value = null; From 3cd158bc7d3855682fb40443e410a7985e154451 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Fri, 5 Jun 2026 18:30:36 -0600 Subject: [PATCH 7/7] fix(Dispatcher): accept devel variants when allow_development_packages is enabled #905 --- .../usr/local/pkg/RESTAPI/Core/Dispatcher.inc | 13 ++- .../Tests/APICoreDispatcherTestCase.inc | 97 +++++++++++++++++++ 2 files changed, 108 insertions(+), 2 deletions(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Dispatcher.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Dispatcher.inc index 5411d4a1..021e7591 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Dispatcher.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Dispatcher.inc @@ -3,6 +3,7 @@ namespace RESTAPI\Core; use RESTAPI\Models\CronJob; +use RESTAPI\Models\RESTAPISettings; use RESTAPI\Responses\FailedDependencyError; use RESTAPI\Responses\ServerError; use RESTAPI\Responses\ServiceUnavailableError; @@ -119,10 +120,18 @@ class Dispatcher { * @throws FailedDependencyError When a package requires a PHP include file that could not be found. */ private function check_required_packages(): void { + # Check if the user has opted in to using development (-devel) package variants + $pkg_config = RESTAPISettings::get_pkg_config(); + $allow_development_packages = ($pkg_config['allow_development_packages'] ?? '') === 'enabled'; + # Loop through each required package and ensure it is present on the system. foreach ($this->required_packages as $pkg) { - # Return an error if the package is not installed - if (!is_pkg_installed($pkg)) { + # When the development packages setting is enabled, also accept the -devel variant of the package + $devel_pkg = $pkg . '-devel'; + $pkg_installed = is_pkg_installed($pkg) || ($allow_development_packages && is_pkg_installed($devel_pkg)); + + # Return an error if neither the package nor its -devel variant (when allowed) is installed + if (!$pkg_installed) { throw new FailedDependencyError( message: "The requested action requires the '$pkg' package but it is not installed.", response_id: 'DISPATCHER_MISSING_REQUIRED_PACKAGE', diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APICoreDispatcherTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APICoreDispatcherTestCase.inc index ceddaf44..52ea30d5 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APICoreDispatcherTestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APICoreDispatcherTestCase.inc @@ -6,6 +6,7 @@ use RESTAPI\Core\Dispatcher; use RESTAPI\Core\TestCase; use RESTAPI\Dispatchers\WireGuardApplyDispatcher; use RESTAPI\Models\Package; +use RESTAPI\Models\RESTAPISettings; class APICoreDispatcherTestCase extends TestCase { /** @@ -176,4 +177,100 @@ class APICoreDispatcherTestCase extends TestCase { # Remove the installed package $package->delete(); } + + /** + * Checks that a Dispatcher requiring a package that is not installed throws DISPATCHER_MISSING_REQUIRED_PACKAGE + * when the `allow_development_packages` setting is disabled (default). + */ + public function test_required_package_missing_throws_by_default(): void { + # Ensure the allow_development_packages setting is disabled + $settings = new RESTAPISettings(); + $settings->from_internal(); + $original = $settings->allow_development_packages->value; + + if ($original) { + $settings->allow_development_packages->value = false; + $settings->update(apply: false); + } + + # A Dispatcher requiring a package that is not installed should fail + $this->assert_throws_response( + response_id: 'DISPATCHER_MISSING_REQUIRED_PACKAGE', + code: 424, + callable: function () { + $dispatcher = new RequiredPackagesTestDispatcher(); + $dispatcher->required_packages = ['pfSense-pkg-some_package_we_dont_have']; + $dispatcher->process(); + }, + ); + } + + /** + * Checks that when `allow_development_packages` is enabled, a Dispatcher whose required package is not installed + * and whose -devel counterpart is also not installed still throws DISPATCHER_MISSING_REQUIRED_PACKAGE. Since we + * cannot install arbitrary packages in tests, we verify the inverse: a package whose -devel name also doesn't + * exist still fails even with the setting enabled. + */ + public function test_devel_variant_still_fails_when_neither_installed(): void { + # Enable the allow_development_packages setting + $settings = new RESTAPISettings(); + $settings->from_internal(); + $settings->allow_development_packages->value = true; + $settings->update(apply: false); + + # Even with the setting on, if neither the base package nor the -devel variant is installed, it should fail + $this->assert_throws_response( + response_id: 'DISPATCHER_MISSING_REQUIRED_PACKAGE', + code: 424, + callable: function () { + $dispatcher = new RequiredPackagesTestDispatcher(); + $dispatcher->required_packages = ['pfSense-pkg-some_package_we_dont_have']; + $dispatcher->process(); + }, + ); + + # Restore the setting to its original value + $settings->allow_development_packages->value = false; + $settings->update(apply: false); + } + + /** + * Checks that when `allow_development_packages` is enabled, a Dispatcher whose required base package is installed + * (using pfSense-pkg-RESTAPI as a proxy for the base package) still passes the required package check. + */ + public function test_base_package_accepted_when_devel_enabled(): void { + # Enable the allow_development_packages setting + $settings = new RESTAPISettings(); + $settings->from_internal(); + $settings->allow_development_packages->value = true; + $settings->update(apply: false); + + # The base package (pfSense-pkg-RESTAPI) is installed; the package check should still pass + $this->assert_does_not_throw( + callable: function () { + $dispatcher = new RequiredPackagesTestDispatcher(); + $dispatcher->required_packages = ['pfSense-pkg-RESTAPI']; + $dispatcher->process(); + }, + ); + + # Restore the setting to its original value + $settings->allow_development_packages->value = false; + $settings->update(apply: false); + } +} + +/** + * Defines a test-only Dispatcher used to exercise the $required_packages check without spawning a real background + * process. The $required_packages property is exposed publicly so individual tests can assign the package(s) to + * check, and _process() is intentionally a no-op so calling process() only runs the required package validation. + */ +class RequiredPackagesTestDispatcher extends Dispatcher { + public array $required_packages = []; + + /** + * Overrides the default _process() with a no-op so tests can validate the package check in isolation. + * @param mixed ...$arguments Unused arguments accepted to match the parent signature. + */ + protected function _process(mixed ...$arguments): void {} }