Compare commits

...
Author SHA1 Message Date
Anthony Stirling 3c600c0d89 Draw pipeline variables as editable boxes instead of braces 2026-08-03 00:24:15 +01:00
Anthony Stirling 5ef2947c01 merge main 2026-08-02 19:05:32 +01:00
dependabot[bot]andAnthony Stirling 5bbe26072f build(deps): bump dompurify from 3.4.11 to 3.4.12 in /frontend (#7133)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.11 to
3.4.12.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/cure53/DOMPurify/releases">dompurify's
releases</a>.</em></p>
<blockquote>
<h2>DOMPurify 3.4.12</h2>
<ul>
<li>Fixed an issue where a hook would not get called for custom
elements, thanks <a
href="https://github.com/Rikuxx0"><code>@​Rikuxx0</code></a></li>
<li>Hardened the handling of hooks removing elements, <a
href="https://github.com/mkrause-bee360"><code>@​mkrause-bee360</code></a></li>
<li>Added support for a few new SVG attributes, thanks <a
href="https://github.com/cbn-falias"><code>@​cbn-falias</code></a> &amp;
<a
href="https://github.com/Develop-KIM"><code>@​Develop-KIM</code></a></li>
<li>Hardened the handling of declarative partial updates</li>
<li>Updated the documentation is several spots, README, wiki, etc.</li>
<li>Bumped several dependencies where possible</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/cure53/DOMPurify/commit/a9ca1e537422319a557a9a2aa61f003b23b4a197"><code>a9ca1e5</code></a>
release: 3.4.12 (<a
href="https://redirect.github.com/cure53/DOMPurify/issues/1537">#1537</a>)</li>
<li>See full diff in <a
href="https://github.com/cure53/DOMPurify/compare/3.4.11...3.4.12">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=dompurify&package-manager=npm_and_yarn&previous-version=3.4.11&new-version=3.4.12)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/Stirling-Tools/Stirling-PDF/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-08-01 23:55:27 +01:00
dependabot[bot] efc6b91648 build(deps): bump docker/login-action from 4.1.0 to 4.5.1 (#7241)
[//]: # (dependabot-start)
⚠️  **Dependabot is rebasing this PR** ⚠️ 

Rebasing might not happen immediately, so don't worry if this takes some
time.

Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.

---

[//]: # (dependabot-end)

Bumps [docker/login-action](https://github.com/docker/login-action) from
4.1.0 to 4.5.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/docker/login-action/releases">docker/login-action's
releases</a>.</em></p>
<blockquote>
<h2>v4.5.1</h2>
<ul>
<li>Support <code>dhi.io</code> as Docker Hub OIDC registry by <a
href="https://github.com/crazy-max"><code>@​crazy-max</code></a> in <a
href="https://redirect.github.com/docker/login-action/pull/1054">docker/login-action#1054</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/docker/login-action/compare/v4.5.0...v4.5.1">https://github.com/docker/login-action/compare/v4.5.0...v4.5.1</a></p>
<h2>v4.5.0</h2>
<ul>
<li><a href="https://github.com/docker/login-action#docker-hub">Docker
Hub OIDC</a> login support by <a
href="https://github.com/crazy-max"><code>@​crazy-max</code></a> in <a
href="https://redirect.github.com/docker/login-action/pull/1048">docker/login-action#1048</a></li>
<li>Bump <code>@​aws-sdk/client-ecr</code> and
<code>@​aws-sdk/client-ecr-public</code> to 3.1091.0 in <a
href="https://redirect.github.com/docker/login-action/pull/1037">docker/login-action#1037</a></li>
<li>Bump <code>@​docker/actions-toolkit</code> from 0.92.0 to 0.94.0 in
<a
href="https://redirect.github.com/docker/login-action/pull/1044">docker/login-action#1044</a>
<a
href="https://redirect.github.com/docker/login-action/pull/1050">docker/login-action#1050</a></li>
<li>Bump brace-expansion from 1.1.13 to 1.1.16 in <a
href="https://redirect.github.com/docker/login-action/pull/1046">docker/login-action#1046</a></li>
<li>Bump js-yaml from 5.2.0 to 5.2.1 in <a
href="https://redirect.github.com/docker/login-action/pull/1038">docker/login-action#1038</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/docker/login-action/compare/v4.4.0...v4.5.0">https://github.com/docker/login-action/compare/v4.4.0...v4.5.0</a></p>
<h2>v4.4.0</h2>
<ul>
<li>Skip empty <code>registry-auth</code> secret mask by <a
href="https://github.com/crazy-max"><code>@​crazy-max</code></a> in <a
href="https://redirect.github.com/docker/login-action/pull/1035">docker/login-action#1035</a></li>
<li>Bump <code>@​aws-sdk/client-ecr</code> and
<code>@​aws-sdk/client-ecr-public</code> to 3.1077.0 <a
href="https://redirect.github.com/docker/login-action/pull/1034">docker/login-action#1034</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/docker/login-action/compare/v4.3.0...v4.4.0">https://github.com/docker/login-action/compare/v4.3.0...v4.4.0</a></p>
<h2>v4.3.0</h2>
<ul>
<li>Preserve names in esbuild bundle by <a
href="https://github.com/crazy-max"><code>@​crazy-max</code></a> in <a
href="https://redirect.github.com/docker/login-action/pull/1022">docker/login-action#1022</a></li>
<li>Bump <code>@​aws-sdk/client-ecr</code> and
<code>@​aws-sdk/client-ecr-public</code> to 3.1076.0 <a
href="https://redirect.github.com/docker/login-action/pull/999">docker/login-action#999</a>
<a
href="https://redirect.github.com/docker/login-action/pull/1030">docker/login-action#1030</a></li>
<li>Bump <code>@​docker/actions-toolkit</code> from 0.90.0 to 0.92.0 in
<a
href="https://redirect.github.com/docker/login-action/pull/1004">docker/login-action#1004</a>
<a
href="https://redirect.github.com/docker/login-action/pull/1027">docker/login-action#1027</a></li>
<li>Bump <code>@​sigstore/core</code> from 3.1.0 to 3.2.1 in <a
href="https://redirect.github.com/docker/login-action/pull/1023">docker/login-action#1023</a></li>
<li>Bump <code>@​sigstore/verify</code> from 3.1.0 to 3.1.1 in <a
href="https://redirect.github.com/docker/login-action/pull/1029">docker/login-action#1029</a></li>
<li>Bump http-proxy-agent and https-proxy-agent to 9.1.0 in <a
href="https://redirect.github.com/docker/login-action/pull/1017">docker/login-action#1017</a></li>
<li>Bump js-yaml from 4.1.1 to 5.2.0 in <a
href="https://redirect.github.com/docker/login-action/pull/1028">docker/login-action#1028</a></li>
<li>Bump sigstore from 4.1.0 to 4.1.1 in <a
href="https://redirect.github.com/docker/login-action/pull/1031">docker/login-action#1031</a></li>
<li>Bump tmp from 0.2.5 to 0.2.7 in <a
href="https://redirect.github.com/docker/login-action/pull/1002">docker/login-action#1002</a></li>
<li>Bump undici from 6.24.1 to 6.27.0 in <a
href="https://redirect.github.com/docker/login-action/pull/1020">docker/login-action#1020</a></li>
<li>Bump vite from 7.3.3 to 7.3.6 in <a
href="https://redirect.github.com/docker/login-action/pull/1019">docker/login-action#1019</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/docker/login-action/compare/v4.2.0...v4.3.0">https://github.com/docker/login-action/compare/v4.2.0...v4.3.0</a></p>
<h2>v4.2.0</h2>
<ul>
<li>Bump <code>@​actions/core</code> from 3.0.0 to 3.0.1 in <a
href="https://redirect.github.com/docker/login-action/pull/976">docker/login-action#976</a></li>
<li>Bump <code>@​aws-sdk/client-ecr</code> and
<code>@​aws-sdk/client-ecr-public</code> to 3.1050.0 in <a
href="https://redirect.github.com/docker/login-action/pull/960">docker/login-action#960</a></li>
<li>Bump <code>@​docker/actions-toolkit</code> from 0.86.0 to 0.90.0 in
<a
href="https://redirect.github.com/docker/login-action/pull/970">docker/login-action#970</a></li>
<li>Bump brace-expansion from 2.0.1 to 5.0.6 in <a
href="https://redirect.github.com/docker/login-action/pull/993">docker/login-action#993</a></li>
<li>Bump fast-xml-builder from 1.1.4 to 1.2.0 in <a
href="https://redirect.github.com/docker/login-action/pull/985">docker/login-action#985</a></li>
<li>Bump fast-xml-parser from 5.3.6 to 5.8.0 in <a
href="https://redirect.github.com/docker/login-action/pull/963">docker/login-action#963</a></li>
<li>Bump http-proxy-agent and https-proxy-agent to 9.0.0 in <a
href="https://redirect.github.com/docker/login-action/pull/961">docker/login-action#961</a></li>
<li>Bump postcss from 8.5.6 to 8.5.10 in <a
href="https://redirect.github.com/docker/login-action/pull/979">docker/login-action#979</a></li>
<li>Bump tar from 6.2.1 to 7.5.15 in <a
href="https://redirect.github.com/docker/login-action/pull/991">docker/login-action#991</a></li>
<li>Bump vite from 7.3.1 to 7.3.3 in <a
href="https://redirect.github.com/docker/login-action/pull/986">docker/login-action#986</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/docker/login-action/compare/v4.1.0...v4.2.0">https://github.com/docker/login-action/compare/v4.1.0...v4.2.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/docker/login-action/commit/abd2ef45e78c5afb21d64d4ca52ee8550d9572c7"><code>abd2ef4</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/login-action/issues/1055">#1055</a>
from crazy-max/test-registry-auth-oidc</li>
<li><a
href="https://github.com/docker/login-action/commit/d49d3a9839fef51322fa44989a44fdc43fccfc22"><code>d49d3a9</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/login-action/issues/1054">#1054</a>
from crazy-max/oidc-missing-dhi</li>
<li><a
href="https://github.com/docker/login-action/commit/b58b17c30b4db92a4ed049b213cae512b12e460b"><code>b58b17c</code></a>
test: cover Docker Hub OIDC with registry-auth</li>
<li><a
href="https://github.com/docker/login-action/commit/be646c21cec26cea303e29290d5f6ba6fde8e606"><code>be646c2</code></a>
chore: update generated content</li>
<li><a
href="https://github.com/docker/login-action/commit/d77c059cb9956cedaa427dc022d89f39acba678f"><code>d77c059</code></a>
support dhi.io as Docker Hub OIDC registry</li>
<li><a
href="https://github.com/docker/login-action/commit/06fb636fac595d6fb4b28a5dfcb21a6f5091859c"><code>06fb636</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/login-action/issues/1037">#1037</a>
from docker/dependabot/npm_and_yarn/aws-sdk-dependen...</li>
<li><a
href="https://github.com/docker/login-action/commit/a8bc9539118a762b0e5788b53a50907977cc1b8d"><code>a8bc953</code></a>
[dependabot skip] chore: update generated content</li>
<li><a
href="https://github.com/docker/login-action/commit/f54b9019bf5074f6e3480a3ac4b834f5f4b90aab"><code>f54b901</code></a>
build(deps): bump the aws-sdk-dependencies group across 1 directory with
2 up...</li>
<li><a
href="https://github.com/docker/login-action/commit/77f18f6713512f90ac35aaf21db0d3710f1b85a6"><code>77f18f6</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/login-action/issues/1049">#1049</a>
from docker/dependabot/github_actions/codeql-actions...</li>
<li><a
href="https://github.com/docker/login-action/commit/ec0bf287fb1e2e051c56b2f6e6a3eed487b9fe52"><code>ec0bf28</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/login-action/issues/1050">#1050</a>
from docker/dependabot/npm_and_yarn/docker/actions-t...</li>
<li>Additional commits viewable in <a
href="https://github.com/docker/login-action/compare/4907a6ddec9925e35a0a9e82d7399ccc52663121...abd2ef45e78c5afb21d64d4ca52ee8550d9572c7">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=docker/login-action&package-manager=github_actions&previous-version=4.1.0&new-version=4.5.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-01 23:42:34 +01:00
dependabot[bot] 0661f90267 build(deps): bump docker/setup-buildx-action from 4.0.0 to 4.2.0 (#7239)
Bumps
[docker/setup-buildx-action](https://github.com/docker/setup-buildx-action)
from 4.0.0 to 4.2.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/docker/setup-buildx-action/releases">docker/setup-buildx-action's
releases</a>.</em></p>
<blockquote>
<h2>v4.2.0</h2>
<ul>
<li>Preserve names in esbuild bundle by <a
href="https://github.com/crazy-max"><code>@​crazy-max</code></a> in <a
href="https://redirect.github.com/docker/setup-buildx-action/pull/572">docker/setup-buildx-action#572</a></li>
<li>Bump <code>@​actions/core</code> from 3.0.0 to 3.0.1 in <a
href="https://redirect.github.com/docker/setup-buildx-action/pull/551">docker/setup-buildx-action#551</a></li>
<li>Bump <code>@​docker/actions-toolkit</code> from 0.90.0 to 0.92.0 in
<a
href="https://redirect.github.com/docker/setup-buildx-action/pull/557">docker/setup-buildx-action#557</a>
<a
href="https://redirect.github.com/docker/setup-buildx-action/pull/580">docker/setup-buildx-action#580</a></li>
<li>Bump <code>@​sigstore/core</code> from 3.1.0 to 3.2.1 in <a
href="https://redirect.github.com/docker/setup-buildx-action/pull/573">docker/setup-buildx-action#573</a></li>
<li>Bump <code>@​sigstore/verify</code> from 3.1.0 to 3.1.1 in <a
href="https://redirect.github.com/docker/setup-buildx-action/pull/576">docker/setup-buildx-action#576</a></li>
<li>Bump js-yaml from 4.1.1 to 5.2.0 in <a
href="https://redirect.github.com/docker/setup-buildx-action/pull/562">docker/setup-buildx-action#562</a></li>
<li>Bump sigstore from 4.1.0 to 4.1.1 in <a
href="https://redirect.github.com/docker/setup-buildx-action/pull/577">docker/setup-buildx-action#577</a></li>
<li>Bump tmp from 0.2.5 to 0.2.7 in <a
href="https://redirect.github.com/docker/setup-buildx-action/pull/556">docker/setup-buildx-action#556</a></li>
<li>Bump undici from 6.25.0 to 6.27.0 in <a
href="https://redirect.github.com/docker/setup-buildx-action/pull/570">docker/setup-buildx-action#570</a></li>
<li>Bump vite from 7.3.2 to 7.3.6 in <a
href="https://redirect.github.com/docker/setup-buildx-action/pull/569">docker/setup-buildx-action#569</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/docker/setup-buildx-action/compare/v4.1.0...v4.2.0">https://github.com/docker/setup-buildx-action/compare/v4.1.0...v4.2.0</a></p>
<h2>v4.1.0</h2>
<ul>
<li>Bump <code>@​docker/actions-toolkit</code> from 0.79.0 to 0.90.0 in
<a
href="https://redirect.github.com/docker/setup-buildx-action/pull/489">docker/setup-buildx-action#489</a></li>
<li>Bump brace-expansion from 1.1.12 to 5.0.6 in <a
href="https://redirect.github.com/docker/setup-buildx-action/pull/547">docker/setup-buildx-action#547</a>
<a
href="https://redirect.github.com/docker/setup-buildx-action/pull/508">docker/setup-buildx-action#508</a></li>
<li>Bump fast-xml-builder from 1.0.0 to 1.2.0 in <a
href="https://redirect.github.com/docker/setup-buildx-action/pull/540">docker/setup-buildx-action#540</a></li>
<li>Bump fast-xml-parser from 5.4.2 to 5.8.0 in <a
href="https://redirect.github.com/docker/setup-buildx-action/pull/496">docker/setup-buildx-action#496</a></li>
<li>Bump flatted from 3.3.3 to 3.4.2 in <a
href="https://redirect.github.com/docker/setup-buildx-action/pull/499">docker/setup-buildx-action#499</a></li>
<li>Bump glob from 10.3.12 to 13.0.6 in <a
href="https://redirect.github.com/docker/setup-buildx-action/pull/495">docker/setup-buildx-action#495</a></li>
<li>Bump handlebars from 4.7.8 to 4.7.9 in <a
href="https://redirect.github.com/docker/setup-buildx-action/pull/504">docker/setup-buildx-action#504</a></li>
<li>Bump lodash from 4.17.23 to 4.18.1 in <a
href="https://redirect.github.com/docker/setup-buildx-action/pull/523">docker/setup-buildx-action#523</a></li>
<li>Bump picomatch from 4.0.3 to 4.0.4 in <a
href="https://redirect.github.com/docker/setup-buildx-action/pull/503">docker/setup-buildx-action#503</a></li>
<li>Bump postcss from 8.5.6 to 8.5.10 in <a
href="https://redirect.github.com/docker/setup-buildx-action/pull/537">docker/setup-buildx-action#537</a></li>
<li>Bump tar from 6.2.1 to 7.5.15 in <a
href="https://redirect.github.com/docker/setup-buildx-action/pull/545">docker/setup-buildx-action#545</a></li>
<li>Bump undici from 6.23.0 to 6.25.0 in <a
href="https://redirect.github.com/docker/setup-buildx-action/pull/492">docker/setup-buildx-action#492</a></li>
<li>Bump vite from 7.3.1 to 7.3.2 in <a
href="https://redirect.github.com/docker/setup-buildx-action/pull/520">docker/setup-buildx-action#520</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/docker/setup-buildx-action/compare/v4.0.0...v4.1.0">https://github.com/docker/setup-buildx-action/compare/v4.0.0...v4.1.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/docker/setup-buildx-action/commit/bb05f3f5519dd87d3ba754cc423b652a5edd6d2c"><code>bb05f3f</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/setup-buildx-action/issues/580">#580</a>
from docker/dependabot/npm_and_yarn/docker/actions-to...</li>
<li><a
href="https://github.com/docker/setup-buildx-action/commit/321c814cb51fbe4af8eca00249525cc0973ea66f"><code>321c814</code></a>
[dependabot skip] chore: update generated content</li>
<li><a
href="https://github.com/docker/setup-buildx-action/commit/b9a36ef79ba42cfc611885a1e8c388fbf8b8cb3f"><code>b9a36ef</code></a>
build(deps): bump <code>@​docker/actions-toolkit</code> from 0.91.0 to
0.92.0</li>
<li><a
href="https://github.com/docker/setup-buildx-action/commit/ebeab241289497cd564ac98b3cfc9e64607bb276"><code>ebeab24</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/setup-buildx-action/issues/570">#570</a>
from docker/dependabot/npm_and_yarn/undici-6.27.0</li>
<li><a
href="https://github.com/docker/setup-buildx-action/commit/5c7b8ae78cec97a3215d4d86679b1d072eaa80cb"><code>5c7b8ae</code></a>
[dependabot skip] chore: update generated content</li>
<li><a
href="https://github.com/docker/setup-buildx-action/commit/037e618cd98e95e81525b15ff0e9c96f507e6a0e"><code>037e618</code></a>
build(deps): bump undici from 6.25.0 to 6.27.0</li>
<li><a
href="https://github.com/docker/setup-buildx-action/commit/66080e5802281ec2e72b7f3108915643e702db85"><code>66080e5</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/setup-buildx-action/issues/577">#577</a>
from docker/dependabot/npm_and_yarn/sigstore-4.1.1</li>
<li><a
href="https://github.com/docker/setup-buildx-action/commit/409aef0aa3f48f0a742e7dec4e0e04ab19afe93c"><code>409aef0</code></a>
Merge pull request <a
href="https://redirect.github.com/docker/setup-buildx-action/issues/562">#562</a>
from docker/dependabot/npm_and_yarn/js-yaml-4.2.0</li>
<li><a
href="https://github.com/docker/setup-buildx-action/commit/49c6e42949280fa0d70fb327633591be54efbfb6"><code>49c6e42</code></a>
build(deps): bump sigstore from 4.1.0 to 4.1.1</li>
<li><a
href="https://github.com/docker/setup-buildx-action/commit/2211273e8121ecf9ecb7d6c7c0fcd55526d530c7"><code>2211273</code></a>
[dependabot skip] chore: update generated content</li>
<li>Additional commits viewable in <a
href="https://github.com/docker/setup-buildx-action/compare/4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd...bb05f3f5519dd87d3ba754cc423b652a5edd6d2c">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=docker/setup-buildx-action&package-manager=github_actions&previous-version=4.0.0&new-version=4.2.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-01 23:41:14 +01:00
dependabot[bot] 02a7db0f1e build(deps): bump pdfboxVersion from 3.0.7 to 3.0.8 (#7237)
[//]: # (dependabot-start)
⚠️  **Dependabot is rebasing this PR** ⚠️ 

Rebasing might not happen immediately, so don't worry if this takes some
time.

Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.

---

[//]: # (dependabot-end)

Bumps `pdfboxVersion` from 3.0.7 to 3.0.8.
Updates `org.apache.pdfbox:preflight` from 3.0.7 to 3.0.8

Updates `org.apache.pdfbox:xmpbox` from 3.0.7 to 3.0.8

Updates `org.apache.pdfbox:pdfbox` from 3.0.7 to 3.0.8

Updates `org.apache.pdfbox:pdfbox-io` from 3.0.7 to 3.0.8


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-01 23:40:47 +01:00
dependabot[bot]andAnthony Stirling 2533baaeb3 build(deps): bump rand from 0.9.2 to 0.10.2 in /frontend/editor/src-tauri (#7065)
Bumps [rand](https://github.com/rust-random/rand) from 0.9.2 to 0.10.2.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/rust-random/rand/blob/master/CHANGELOG.md">rand's
changelog</a>.</em></p>
<blockquote>
<h2>[0.10.2] — 2026-07-02</h2>
<h3>Fixes</h3>
<ul>
<li>Fix possible memory safety violation due to deserialization of
<code>UniformChar</code> from bad source (<a
href="https://redirect.github.com/rust-random/rand/issues/1790">#1790</a>)</li>
</ul>
<h3>Changes</h3>
<ul>
<li>Document required output order of fn <code>partial_shuffle</code>
and apply <code>#[must_use]</code> (<a
href="https://redirect.github.com/rust-random/rand/issues/1769">#1769</a>)</li>
<li>Avoid usage of <code>unsafe</code> in contexts where non-local
memory corruption could invalidate contract (<a
href="https://redirect.github.com/rust-random/rand/issues/1791">#1791</a>)</li>
</ul>
<p><a
href="https://redirect.github.com/rust-random/rand/issues/1769">#1769</a>:
<a
href="https://redirect.github.com/rust-random/rand/pull/1769">rust-random/rand#1769</a>
<a
href="https://redirect.github.com/rust-random/rand/issues/1790">#1790</a>:
<a
href="https://redirect.github.com/rust-random/rand/pull/1790">rust-random/rand#1790</a>
<a
href="https://redirect.github.com/rust-random/rand/issues/1791">#1791</a>:
<a
href="https://redirect.github.com/rust-random/rand/pull/1791">rust-random/rand#1791</a></p>
<h2>[0.10.1] — 2026-02-11</h2>
<p>This release includes a fix for a soundness bug; see <a
href="https://redirect.github.com/rust-random/rand/issues/1763">#1763</a>.</p>
<h3>Changes</h3>
<ul>
<li>Document panic behavior of <code>make_rng</code> and add
<code>#[track_caller]</code> (<a
href="https://redirect.github.com/rust-random/rand/issues/1761">#1761</a>)</li>
<li>Deprecate feature <code>log</code> (<a
href="https://redirect.github.com/rust-random/rand/issues/1763">#1763</a>)</li>
</ul>
<p><a
href="https://redirect.github.com/rust-random/rand/issues/1761">#1761</a>:
<a
href="https://redirect.github.com/rust-random/rand/pull/1761">rust-random/rand#1761</a>
<a
href="https://redirect.github.com/rust-random/rand/issues/1763">#1763</a>:
<a
href="https://redirect.github.com/rust-random/rand/pull/1763">rust-random/rand#1763</a></p>
<h2>[0.10.0] - 2026-02-08</h2>
<h3>Changes</h3>
<ul>
<li>The dependency on <code>rand_chacha</code> has been replaced with a
dependency on <code>chacha20</code>. This changes the implementation
behind <code>StdRng</code>, but the output remains the same. There may
be some API breakage when using the ChaCha-types directly as these are
now the ones in <code>chacha20</code> instead of
<code>rand_chacha</code> (<a
href="https://redirect.github.com/rust-random/rand/issues/1642">#1642</a>).</li>
<li>Rename fns <code>IndexedRandom::choose_multiple</code> -&gt;
<code>sample</code>, <code>choose_multiple_array</code> -&gt;
<code>sample_array</code>, <code>choose_multiple_weighted</code> -&gt;
<code>sample_weighted</code>, struct <code>SliceChooseIter</code> -&gt;
<code>IndexedSamples</code> and fns
<code>IteratorRandom::choose_multiple</code> -&gt; <code>sample</code>,
<code>choose_multiple_fill</code> -&gt; <code>sample_fill</code> (<a
href="https://redirect.github.com/rust-random/rand/issues/1632">#1632</a>)</li>
<li>Use Edition 2024 and MSRV 1.85 (<a
href="https://redirect.github.com/rust-random/rand/issues/1653">#1653</a>)</li>
<li>Let <code>Fill</code> be implemented for element types, not
sliceable types (<a
href="https://redirect.github.com/rust-random/rand/issues/1652">#1652</a>)</li>
<li>Fix <code>OsError::raw_os_error</code> on UEFI targets by returning
<code>Option&lt;usize&gt;</code> (<a
href="https://redirect.github.com/rust-random/rand/issues/1665">#1665</a>)</li>
<li>Replace fn <code>TryRngCore::read_adapter(..) -&gt;
RngReadAdapter</code> with simpler struct <code>RngReader</code> (<a
href="https://redirect.github.com/rust-random/rand/issues/1669">#1669</a>)</li>
<li>Remove fns <code>SeedableRng::from_os_rng</code>,
<code>try_from_os_rng</code> (<a
href="https://redirect.github.com/rust-random/rand/issues/1674">#1674</a>)</li>
<li>Remove <code>Clone</code> support for <code>StdRng</code>,
<code>ReseedingRng</code> (<a
href="https://redirect.github.com/rust-random/rand/issues/1677">#1677</a>)</li>
<li>Use <code>postcard</code> instead of <code>bincode</code> to test
the serde feature (<a
href="https://redirect.github.com/rust-random/rand/issues/1693">#1693</a>)</li>
<li>Avoid excessive allocation in <code>IteratorRandom::sample</code>
when <code>amount</code> is much larger than iterator size (<a
href="https://redirect.github.com/rust-random/rand/issues/1695">#1695</a>)</li>
<li>Rename <code>os_rng</code> -&gt; <code>sys_rng</code>,
<code>OsRng</code> -&gt; <code>SysRng</code>, <code>OsError</code> -&gt;
<code>SysError</code> (<a
href="https://redirect.github.com/rust-random/rand/issues/1697">#1697</a>)</li>
<li>Rename <code>Rng</code> -&gt; <code>RngExt</code> as upstream
<code>rand_core</code> has renamed <code>RngCore</code> -&gt;
<code>Rng</code> (<a
href="https://redirect.github.com/rust-random/rand/issues/1717">#1717</a>)</li>
</ul>
<h3>Additions</h3>
<ul>
<li>Add fns <code>IndexedRandom::choose_iter</code>,
<code>choose_weighted_iter</code> (<a
href="https://redirect.github.com/rust-random/rand/issues/1632">#1632</a>)</li>
<li>Pub export <code>Xoshiro128PlusPlus</code>,
<code>Xoshiro256PlusPlus</code> prngs (<a
href="https://redirect.github.com/rust-random/rand/issues/1649">#1649</a>)</li>
<li>Pub export <code>ChaCha8Rng</code>, <code>ChaCha12Rng</code>,
<code>ChaCha20Rng</code> behind <code>chacha</code> feature (<a
href="https://redirect.github.com/rust-random/rand/issues/1659">#1659</a>)</li>
<li>Fn <code>rand::make_rng() -&gt; R where R: SeedableRng</code> (<a
href="https://redirect.github.com/rust-random/rand/issues/1734">#1734</a>)</li>
</ul>
<h3>Removals</h3>
<ul>
<li>Removed <code>ReseedingRng</code> (<a
href="https://redirect.github.com/rust-random/rand/issues/1722">#1722</a>)</li>
<li>Removed unused feature &quot;nightly&quot; (<a
href="https://redirect.github.com/rust-random/rand/issues/1732">#1732</a>)</li>
<li>Removed feature <code>small_rng</code> (<a
href="https://redirect.github.com/rust-random/rand/issues/1732">#1732</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/rust-random/rand/commit/1540ea327e8beaf0694ea64f6d9eb8eaadd47bd5"><code>1540ea3</code></a>
Prepare rand 0.10.2 (<a
href="https://redirect.github.com/rust-random/rand/issues/1800">#1800</a>)</li>
<li><a
href="https://github.com/rust-random/rand/commit/a29964ad94b54c25b3865626de6964ce0f796a29"><code>a29964a</code></a>
Bump chacha20 from 0.10.0 to 0.10.1 in the all-deps group (<a
href="https://redirect.github.com/rust-random/rand/issues/1801">#1801</a>)</li>
<li><a
href="https://github.com/rust-random/rand/commit/ced94914cb75c93a1f19140a966a466345185fff"><code>ced9491</code></a>
Tweak docs for RngExt::random_range and SampleRange (<a
href="https://redirect.github.com/rust-random/rand/issues/1798">#1798</a>)</li>
<li><a
href="https://github.com/rust-random/rand/commit/db146647afaf002b866420d34e4501b0dd872163"><code>db14664</code></a>
Check UniformChar validity on deser (<a
href="https://redirect.github.com/rust-random/rand/issues/1790">#1790</a>)</li>
<li><a
href="https://github.com/rust-random/rand/commit/bea8620204c7aeecdefc132b5cb0dec8134add4b"><code>bea8620</code></a>
Bump the all-deps group with 2 updates (<a
href="https://redirect.github.com/rust-random/rand/issues/1796">#1796</a>)</li>
<li><a
href="https://github.com/rust-random/rand/commit/4f449322825498e4ec1f486119e5fd251ba97f8a"><code>4f44932</code></a>
Bump actions/cache from 5 to 6 (<a
href="https://redirect.github.com/rust-random/rand/issues/1795">#1795</a>)</li>
<li><a
href="https://github.com/rust-random/rand/commit/b999a130a990b30af01743021e8ea97f3b09a17e"><code>b999a13</code></a>
Bump actions/checkout from 6 to 7 (<a
href="https://redirect.github.com/rust-random/rand/issues/1794">#1794</a>)</li>
<li><a
href="https://github.com/rust-random/rand/commit/aeab810bd9704a3b7666ba0a78e1ad5d1d5ad1ae"><code>aeab810</code></a>
Avoid unsafe where safety depends on non-local values (<a
href="https://redirect.github.com/rust-random/rand/issues/1791">#1791</a>)</li>
<li><a
href="https://github.com/rust-random/rand/commit/1896d7c660524a022b3dbc3a1e044e162d766b25"><code>1896d7c</code></a>
Add typos CI job (<a
href="https://redirect.github.com/rust-random/rand/issues/1789">#1789</a>)</li>
<li><a
href="https://github.com/rust-random/rand/commit/43eddee18c8cca2cebee929be3899cf183afe801"><code>43eddee</code></a>
Bump the all-deps group with 2 updates (<a
href="https://redirect.github.com/rust-random/rand/issues/1788">#1788</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/rust-random/rand/compare/rand_core-0.9.2...0.10.2">compare
view</a></li>
</ul>
</details>
<br />

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-08-01 23:40:30 +01:00
dependabot[bot]andAnthony Stirling ced8e5e52c build(deps): bump eclipse-temurin from b27ca47 to f9bd881 in /docker/embedded (#6556)
Bumps eclipse-temurin from `b27ca47` to `f9bd881`.


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=eclipse-temurin&package-manager=docker&previous-version=25-jre-alpine&new-version=25-jre-alpine)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

You can trigger a rebase of this PR by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

> **Note**
> Automatic rebases have been disabled on this pull request as it has
been open for over 30 days.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-08-01 23:39:36 +01:00
dependabot[bot]andAnthony Stirling e4c9b0410c build(deps): bump org.verapdf:validation-model from 1.28.2 to 1.30.2 in /app/core (#6836)
Bumps
[org.verapdf:validation-model](https://github.com/veraPDF/veraPDF-validation)
from 1.28.2 to 1.30.2.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/veraPDF/veraPDF-validation/commit/94caa46c1a594512247fbd46c808edae39469542"><code>94caa46</code></a>
Improve security of the DocumentBuilder</li>
<li><a
href="https://github.com/veraPDF/veraPDF-validation/commit/ae243fc06775ef79495accd2165e5262e31dcf67"><code>ae243fc</code></a>
Fix typo</li>
<li><a
href="https://github.com/veraPDF/veraPDF-validation/commit/f89130de3b97f19ef4030e5fc84345c4463cd867"><code>f89130d</code></a>
Fix getAlt in GFPDAnnot</li>
<li><a
href="https://github.com/veraPDF/veraPDF-validation/commit/11ad0c577853a658d11370277196c12e34b5fd81"><code>11ad0c5</code></a>
Proposed SECURITY.md file for veraPDF projects</li>
<li><a
href="https://github.com/veraPDF/veraPDF-validation/commit/59f9cd4c97799dd87d738ce847c066530bcf2ed1"><code>59f9cd4</code></a>
Use non static isCircularMappingExist</li>
<li><a
href="https://github.com/veraPDF/veraPDF-validation/commit/7494455aa44fd322fa7bd900456a3cb8f0602f18"><code>7494455</code></a>
REL - v1.30</li>
<li><a
href="https://github.com/veraPDF/veraPDF-validation/commit/ecd191d3d796dfdfa13970a114920530627a25bb"><code>ecd191d</code></a>
Fix glyph name detection for symbolic TrueType font</li>
<li><a
href="https://github.com/veraPDF/veraPDF-validation/commit/32e21ce19a478a4e8e47c4a090674cbb2cb58026"><code>32e21ce</code></a>
Update fixRevProperty method (<a
href="https://redirect.github.com/veraPDF/veraPDF-validation/issues/726">#726</a>)</li>
<li><a
href="https://github.com/veraPDF/veraPDF-validation/commit/ddfee10116d45cc37750a988ff4385dad0c8510a"><code>ddfee10</code></a>
PDF/UA. Fix Table validation</li>
<li><a
href="https://github.com/veraPDF/veraPDF-validation/commit/22ea95799bb98b50a2063c542de50de4aeb4ce9e"><code>22ea957</code></a>
RC - v1.30</li>
<li>Additional commits viewable in <a
href="https://github.com/veraPDF/veraPDF-validation/compare/v1.28.2...v1.30.2">compare
view</a></li>
</ul>
</details>
<br />


> **Note**
> Automatic rebases have been disabled on this pull request as it has
been open for over 30 days.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-08-01 23:39:20 +01:00
dependabot[bot] 53086a12c2 build(deps): bump serde_with from 3.17.0 to 3.21.0 in /frontend/editor/src-tauri (#7053)
Bumps [serde_with](https://github.com/jonasbb/serde_with) from 3.17.0 to
3.21.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/jonasbb/serde_with/releases">serde_with's
releases</a>.</em></p>
<blockquote>
<h2>serde_with v3.21.0</h2>
<h3>Security</h3>
<ul>
<li>
<p><a
href="https://github.com/jonasbb/serde_with/security/advisories/GHSA-7gcf-g7xr-8hxj">GHSA-7gcf-g7xr-8hxj</a>:
KeyValueMap serialization panics on empty sequence or map entries
Bad or attacker controlled values could cause a panic while allocating
too large values.
Fixed in <a
href="https://redirect.github.com/jonasbb/serde_with/issues/966">#966</a>
by setting a maximum allocation size during the creation of collections
like <code>Vec</code> or sets.</p>
<p>Thanks to <a
href="https://github.com/7thParkk"><code>@​7thParkk</code></a> for
reporting the issue.</p>
</li>
</ul>
<h3>Added</h3>
<ul>
<li>Add <code>NoneAsZero</code> adapter that maps
<code>Option&lt;NonZero*&gt;</code> to a plain integer, encoding
<code>None</code> as <code>0</code> by <a
href="https://github.com/SAY-5"><code>@​SAY-5</code></a> (<a
href="https://redirect.github.com/jonasbb/serde_with/issues/486">#486</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>Re-enable link-to-definition on docs.rs (<a
href="https://redirect.github.com/jonasbb/serde_with/issues/964">#964</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Fix some doc links to point to the correct types (<a
href="https://redirect.github.com/jonasbb/serde_with/issues/963">#963</a>)</li>
<li>Re-enable <code>unused_qualifications</code> and fix the resulting
findings by <a
href="https://github.com/lms0806"><code>@​lms0806</code></a> (<a
href="https://redirect.github.com/jonasbb/serde_with/issues/962">#962</a>)</li>
</ul>
<h2>serde_with v3.20.0</h2>
<h3>Added</h3>
<ul>
<li>Add support for <code>base58</code> encoding, similar to the
existing <code>base64</code> setup by <a
href="https://github.com/mitinarseny"><code>@​mitinarseny</code></a> (<a
href="https://redirect.github.com/jonasbb/serde_with/issues/943">#943</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Extend <code>base64</code> with <code>schemars</code> support by <a
href="https://github.com/mitinarseny"><code>@​mitinarseny</code></a> (<a
href="https://redirect.github.com/jonasbb/serde_with/issues/9949">#9949</a>)</li>
</ul>
<h2>serde_with v3.19.0</h2>
<h3>Added</h3>
<ul>
<li>
<p>Add support for <code>hashbrown</code> v0.17 (<a
href="https://redirect.github.com/jonasbb/serde_with/issues/940">#940</a>)</p>
<p>This extends the existing support for <code>hashbrown</code> to the
newly released version.</p>
</li>
</ul>
<h2>serde_with v3.18.0</h2>
<h3>Added</h3>
<ul>
<li>Support <code>OneOrMany</code> with more sequence and set types (<a
href="https://redirect.github.com/jonasbb/serde_with/issues/929">#929</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>Bump MSRV to 1.88 due to the <code>darling</code> dependency</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/jonasbb/serde_with/commit/0f4ca67e1f8fc4679e850f3a566d454fb30953c1"><code>0f4ca67</code></a>
Update changelog for 3.21.0 (<a
href="https://redirect.github.com/jonasbb/serde_with/issues/967">#967</a>)</li>
<li><a
href="https://github.com/jonasbb/serde_with/commit/7654841be1d1702a65afc0f839c67c36563c8188"><code>7654841</code></a>
Update changelog for 3.21.0</li>
<li><a
href="https://github.com/jonasbb/serde_with/commit/c8a1d820ea25df01692b367058d587343e199389"><code>c8a1d82</code></a>
Protect all collection creations against capacity overflow by using
`size_hin...</li>
<li><a
href="https://github.com/jonasbb/serde_with/commit/6ad5fa5b474270f50016b4cc983e37f25f097ba4"><code>6ad5fa5</code></a>
Properly feature gate the <code>vec_with_capacity_cautious</code>
function</li>
<li><a
href="https://github.com/jonasbb/serde_with/commit/ef7d1417e3eacd0077f029763109368ee05c1c22"><code>ef7d141</code></a>
Protect all collection creations against capacity overflow by using
`size_hin...</li>
<li><a
href="https://github.com/jonasbb/serde_with/commit/a348da35fe808852a1b7e6fa890b425ad001d3f1"><code>a348da3</code></a>
Add serde_as deserialize_as explain (<a
href="https://redirect.github.com/jonasbb/serde_with/issues/958">#958</a>)</li>
<li><a
href="https://github.com/jonasbb/serde_with/commit/2e5bc20e29e1d42eb9c85ab503964130eb1ea62e"><code>2e5bc20</code></a>
Bump the github-actions group with 3 updates (<a
href="https://redirect.github.com/jonasbb/serde_with/issues/965">#965</a>)</li>
<li><a
href="https://github.com/jonasbb/serde_with/commit/927a3d69c3cecdf415f7d7662a0521894d313261"><code>927a3d6</code></a>
Bump the github-actions group with 3 updates</li>
<li><a
href="https://github.com/jonasbb/serde_with/commit/62d14ec637834259e0fab59ea84b87ca329e81c1"><code>62d14ec</code></a>
Enable link-to-definition on docs.rs again, after the upstream issue was
reso...</li>
<li><a
href="https://github.com/jonasbb/serde_with/commit/4584d94f685b66b96bdcf07bffe76e5df0819ea2"><code>4584d94</code></a>
Enable link-to-definition on docs.rs again, after the upstream issue was
reso...</li>
<li>Additional commits viewable in <a
href="https://github.com/jonasbb/serde_with/compare/v3.17.0...v3.21.0">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-01 22:42:31 +01:00
dependabot[bot]andAnthony Stirling 4d6c8341c8 build(deps): bump quinn-proto from 0.11.14 to 0.11.16 in /frontend/editor/src-tauri (#7179)
Bumps [quinn-proto](https://github.com/quinn-rs/quinn) from 0.11.14 to
0.11.16.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/quinn-rs/quinn/releases">quinn-proto's
releases</a>.</em></p>
<blockquote>
<h2>quinn-proto-0.11.16</h2>
<h2>What's Changed</h2>
<ul>
<li>0.11.x: upgrade dependencies by <a
href="https://github.com/djc"><code>@​djc</code></a> in <a
href="https://redirect.github.com/quinn-rs/quinn/pull/2707">quinn-rs/quinn#2707</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/quinn-rs/quinn/commit/a96949f6cd257c665f544626af4e8ce668a40b30"><code>a96949f</code></a>
Take semver-compatible update for anyhow</li>
<li><a
href="https://github.com/quinn-rs/quinn/commit/5429f60d0ee9971770e60f0407d992d3b912d274"><code>5429f60</code></a>
udp: bump version to 0.5.15</li>
<li><a
href="https://github.com/quinn-rs/quinn/commit/262a493629acdc979070cd36d801464a2b8dd3e2"><code>262a493</code></a>
proto: bump version to 0.11.16</li>
<li><a
href="https://github.com/quinn-rs/quinn/commit/c19b63a04c6eff60684a845fce29fee6d74b1acd"><code>c19b63a</code></a>
Upgrade rustls-platform-verifier to 0.7</li>
<li><a
href="https://github.com/quinn-rs/quinn/commit/aff3652c43be3491908ef553fac16610c9ad3e6a"><code>aff3652</code></a>
Disable default features for fastbloom</li>
<li><a
href="https://github.com/quinn-rs/quinn/commit/01b2eee2c68b1d73ad09485b440fbfa8f6d3e290"><code>01b2eee</code></a>
Upgrade fastbloom to 0.17</li>
<li><a
href="https://github.com/quinn-rs/quinn/commit/2c82013a8cd502f3dd0bba0a4c38a51e564d29cc"><code>2c82013</code></a>
Switch BBR RNG to PCG</li>
<li><a
href="https://github.com/quinn-rs/quinn/commit/544dd9ebabf18639cb2041f849ea406100dd3d6d"><code>544dd9e</code></a>
Upgrade to rand 0.10.1</li>
<li><a
href="https://github.com/quinn-rs/quinn/commit/a7499b8439e393a6299330111d9c8564cd96c464"><code>a7499b8</code></a>
Bump versions for release</li>
<li><a
href="https://github.com/quinn-rs/quinn/commit/7c1970f19b24280af86b1a0a1c2d06d59fc453f0"><code>7c1970f</code></a>
proto: yield error on too many gaps in assembler</li>
<li>Additional commits viewable in <a
href="https://github.com/quinn-rs/quinn/compare/quinn-proto-0.11.14...quinn-proto-0.11.16">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=quinn-proto&package-manager=cargo&previous-version=0.11.14&new-version=0.11.16)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/Stirling-Tools/Stirling-PDF/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-08-01 22:42:14 +01:00
dependabot[bot]andAnthony Stirling 113a5c7372 build(deps): bump the serde group across 2 directories with 2 updates (#7182)
Bumps the serde group with 2 updates in the /frontend/editor/src-tauri
directory: [serde_json](https://github.com/serde-rs/json) and
[serde](https://github.com/serde-rs/serde).
Bumps the serde group with 2 updates in the
/frontend/editor/src-tauri/provisioner directory:
[serde_json](https://github.com/serde-rs/json) and
[serde](https://github.com/serde-rs/serde).

Updates `serde_json` from 1.0.149 to 1.0.151
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/serde-rs/json/releases">serde_json's
releases</a>.</em></p>
<blockquote>
<h2>v1.0.151</h2>
<ul>
<li>Add RawValue::from_string_unchecked (<a
href="https://redirect.github.com/serde-rs/json/issues/1331">#1331</a>,
thanks <a
href="https://github.com/WonderLawrence"><code>@​WonderLawrence</code></a>)</li>
</ul>
<h2>v1.0.150</h2>
<ul>
<li>Reject non-string enum object keys (<a
href="https://redirect.github.com/serde-rs/json/issues/1324">#1324</a>,
thanks <a
href="https://github.com/puneetdixit200"><code>@​puneetdixit200</code></a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/serde-rs/json/commit/de8500740cdcabffb9734f503e4889def823cf10"><code>de85007</code></a>
Release 1.0.151</li>
<li><a
href="https://github.com/serde-rs/json/commit/3b2b3c5f28c20ed988bd081a4147c535e7e65c74"><code>3b2b3c5</code></a>
Merge pull request <a
href="https://redirect.github.com/serde-rs/json/issues/1331">#1331</a>
from WonderLawrence/rawvalue-from-string-unchecked</li>
<li><a
href="https://github.com/serde-rs/json/commit/0406d96860e9d8b9252e2002fa3e626ae48ca1b0"><code>0406d96</code></a>
Debug-assert well-formedness and no-whitespace in
from_string_unchecked</li>
<li><a
href="https://github.com/serde-rs/json/commit/cf16f75d81e28c723323bfc60a68fc02d2994fff"><code>cf16f75</code></a>
Add RawValue::from_string_unchecked</li>
<li><a
href="https://github.com/serde-rs/json/commit/827a315bf2198558f0325b07bcc1e2cd973aba2f"><code>827a315</code></a>
Update actions/upload-artifact@v6 -&gt; v7</li>
<li><a
href="https://github.com/serde-rs/json/commit/cea36a5c017ebffdeb95d0cd0f1aad473bfab758"><code>cea36a5</code></a>
Update actions/checkout@v6 -&gt; v7</li>
<li><a
href="https://github.com/serde-rs/json/commit/a1ae73ac6a6940a4a57c673aebaa13ed4dfe3e8c"><code>a1ae73a</code></a>
Release 1.0.150</li>
<li><a
href="https://github.com/serde-rs/json/commit/1a360b0a6c003912afc3503c834b0edd798bca28"><code>1a360b0</code></a>
Merge pull request <a
href="https://redirect.github.com/serde-rs/json/issues/1324">#1324</a>
from puneetdixit200/reject-non-string-enum-keys</li>
<li><a
href="https://github.com/serde-rs/json/commit/2037b634f9dccbddc11cff189ebeb5854fa0e01c"><code>2037b63</code></a>
Reject non-string enum object keys</li>
<li><a
href="https://github.com/serde-rs/json/commit/5d30df60e916e9b8fc46c74794007ff271fdfbbf"><code>5d30df6</code></a>
Resolve manual_assert_eq pedantic clippy lint</li>
<li>Additional commits viewable in <a
href="https://github.com/serde-rs/json/compare/v1.0.149...v1.0.151">compare
view</a></li>
</ul>
</details>
<br />

Updates `serde` from 1.0.228 to 1.0.229
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/serde-rs/serde/releases">serde's
releases</a>.</em></p>
<blockquote>
<h2>v1.0.229</h2>
<ul>
<li>Update to syn 3</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/serde-rs/serde/commit/7fc3b4c30c94f73a96ebd1553f2b090d928fc3a8"><code>7fc3b4c</code></a>
Release 1.0.229</li>
<li><a
href="https://github.com/serde-rs/serde/commit/6d6e9a11101354ce769a3438a088b6b9305c1863"><code>6d6e9a1</code></a>
Merge pull request <a
href="https://redirect.github.com/serde-rs/serde/issues/3085">#3085</a>
from dtolnay/syn3</li>
<li><a
href="https://github.com/serde-rs/serde/commit/6dec3b751126c8338cac0fe8085612d695e4ecf3"><code>6dec3b7</code></a>
Update to syn 3</li>
<li><a
href="https://github.com/serde-rs/serde/commit/cfe669241065984177ff63af8b45058e6e9b499d"><code>cfe6692</code></a>
Resolve mut_mut pedantic clippy lint</li>
<li><a
href="https://github.com/serde-rs/serde/commit/1023d077510b4aef36a41ef56fdb7798568a2654"><code>1023d07</code></a>
Update actions/upload-artifact@v6 -&gt; v7</li>
<li><a
href="https://github.com/serde-rs/serde/commit/dd682c2c86aa7629e77c1ccd93212d3729f4c66d"><code>dd682c2</code></a>
Update actions/checkout@v6 -&gt; v7</li>
<li><a
href="https://github.com/serde-rs/serde/commit/5f0f18b9211732f2d82f73b5a43e4f5ff3701251"><code>5f0f18b</code></a>
Update ui test suite to nightly-2026-06-01</li>
<li><a
href="https://github.com/serde-rs/serde/commit/63a1498f0e7be991ffac5939bdd202ca16e9a23f"><code>63a1498</code></a>
Regenerate stderr with trybuild normalization fixes</li>
<li><a
href="https://github.com/serde-rs/serde/commit/fa7da4a93567ed347ad0735c28e439fca688ef26"><code>fa7da4a</code></a>
Fix unused_features warning</li>
<li><a
href="https://github.com/serde-rs/serde/commit/6b1a17851ea3d86a56aa116ca1cbf428f8d5f22d"><code>6b1a178</code></a>
Unpin CI miri toolchain</li>
<li>Additional commits viewable in <a
href="https://github.com/serde-rs/serde/compare/v1.0.228...v1.0.229">compare
view</a></li>
</ul>
</details>
<br />

Updates `serde` from 1.0.228 to 1.0.229
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/serde-rs/serde/releases">serde's
releases</a>.</em></p>
<blockquote>
<h2>v1.0.229</h2>
<ul>
<li>Update to syn 3</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/serde-rs/serde/commit/7fc3b4c30c94f73a96ebd1553f2b090d928fc3a8"><code>7fc3b4c</code></a>
Release 1.0.229</li>
<li><a
href="https://github.com/serde-rs/serde/commit/6d6e9a11101354ce769a3438a088b6b9305c1863"><code>6d6e9a1</code></a>
Merge pull request <a
href="https://redirect.github.com/serde-rs/serde/issues/3085">#3085</a>
from dtolnay/syn3</li>
<li><a
href="https://github.com/serde-rs/serde/commit/6dec3b751126c8338cac0fe8085612d695e4ecf3"><code>6dec3b7</code></a>
Update to syn 3</li>
<li><a
href="https://github.com/serde-rs/serde/commit/cfe669241065984177ff63af8b45058e6e9b499d"><code>cfe6692</code></a>
Resolve mut_mut pedantic clippy lint</li>
<li><a
href="https://github.com/serde-rs/serde/commit/1023d077510b4aef36a41ef56fdb7798568a2654"><code>1023d07</code></a>
Update actions/upload-artifact@v6 -&gt; v7</li>
<li><a
href="https://github.com/serde-rs/serde/commit/dd682c2c86aa7629e77c1ccd93212d3729f4c66d"><code>dd682c2</code></a>
Update actions/checkout@v6 -&gt; v7</li>
<li><a
href="https://github.com/serde-rs/serde/commit/5f0f18b9211732f2d82f73b5a43e4f5ff3701251"><code>5f0f18b</code></a>
Update ui test suite to nightly-2026-06-01</li>
<li><a
href="https://github.com/serde-rs/serde/commit/63a1498f0e7be991ffac5939bdd202ca16e9a23f"><code>63a1498</code></a>
Regenerate stderr with trybuild normalization fixes</li>
<li><a
href="https://github.com/serde-rs/serde/commit/fa7da4a93567ed347ad0735c28e439fca688ef26"><code>fa7da4a</code></a>
Fix unused_features warning</li>
<li><a
href="https://github.com/serde-rs/serde/commit/6b1a17851ea3d86a56aa116ca1cbf428f8d5f22d"><code>6b1a178</code></a>
Unpin CI miri toolchain</li>
<li>Additional commits viewable in <a
href="https://github.com/serde-rs/serde/compare/v1.0.228...v1.0.229">compare
view</a></li>
</ul>
</details>
<br />

Updates `serde_json` from 1.0.149 to 1.0.151
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/serde-rs/json/releases">serde_json's
releases</a>.</em></p>
<blockquote>
<h2>v1.0.151</h2>
<ul>
<li>Add RawValue::from_string_unchecked (<a
href="https://redirect.github.com/serde-rs/json/issues/1331">#1331</a>,
thanks <a
href="https://github.com/WonderLawrence"><code>@​WonderLawrence</code></a>)</li>
</ul>
<h2>v1.0.150</h2>
<ul>
<li>Reject non-string enum object keys (<a
href="https://redirect.github.com/serde-rs/json/issues/1324">#1324</a>,
thanks <a
href="https://github.com/puneetdixit200"><code>@​puneetdixit200</code></a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/serde-rs/json/commit/de8500740cdcabffb9734f503e4889def823cf10"><code>de85007</code></a>
Release 1.0.151</li>
<li><a
href="https://github.com/serde-rs/json/commit/3b2b3c5f28c20ed988bd081a4147c535e7e65c74"><code>3b2b3c5</code></a>
Merge pull request <a
href="https://redirect.github.com/serde-rs/json/issues/1331">#1331</a>
from WonderLawrence/rawvalue-from-string-unchecked</li>
<li><a
href="https://github.com/serde-rs/json/commit/0406d96860e9d8b9252e2002fa3e626ae48ca1b0"><code>0406d96</code></a>
Debug-assert well-formedness and no-whitespace in
from_string_unchecked</li>
<li><a
href="https://github.com/serde-rs/json/commit/cf16f75d81e28c723323bfc60a68fc02d2994fff"><code>cf16f75</code></a>
Add RawValue::from_string_unchecked</li>
<li><a
href="https://github.com/serde-rs/json/commit/827a315bf2198558f0325b07bcc1e2cd973aba2f"><code>827a315</code></a>
Update actions/upload-artifact@v6 -&gt; v7</li>
<li><a
href="https://github.com/serde-rs/json/commit/cea36a5c017ebffdeb95d0cd0f1aad473bfab758"><code>cea36a5</code></a>
Update actions/checkout@v6 -&gt; v7</li>
<li><a
href="https://github.com/serde-rs/json/commit/a1ae73ac6a6940a4a57c673aebaa13ed4dfe3e8c"><code>a1ae73a</code></a>
Release 1.0.150</li>
<li><a
href="https://github.com/serde-rs/json/commit/1a360b0a6c003912afc3503c834b0edd798bca28"><code>1a360b0</code></a>
Merge pull request <a
href="https://redirect.github.com/serde-rs/json/issues/1324">#1324</a>
from puneetdixit200/reject-non-string-enum-keys</li>
<li><a
href="https://github.com/serde-rs/json/commit/2037b634f9dccbddc11cff189ebeb5854fa0e01c"><code>2037b63</code></a>
Reject non-string enum object keys</li>
<li><a
href="https://github.com/serde-rs/json/commit/5d30df60e916e9b8fc46c74794007ff271fdfbbf"><code>5d30df6</code></a>
Resolve manual_assert_eq pedantic clippy lint</li>
<li>Additional commits viewable in <a
href="https://github.com/serde-rs/json/compare/v1.0.149...v1.0.151">compare
view</a></li>
</ul>
</details>
<br />

Updates `serde_json` from 1.0.149 to 1.0.151
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/serde-rs/json/releases">serde_json's
releases</a>.</em></p>
<blockquote>
<h2>v1.0.151</h2>
<ul>
<li>Add RawValue::from_string_unchecked (<a
href="https://redirect.github.com/serde-rs/json/issues/1331">#1331</a>,
thanks <a
href="https://github.com/WonderLawrence"><code>@​WonderLawrence</code></a>)</li>
</ul>
<h2>v1.0.150</h2>
<ul>
<li>Reject non-string enum object keys (<a
href="https://redirect.github.com/serde-rs/json/issues/1324">#1324</a>,
thanks <a
href="https://github.com/puneetdixit200"><code>@​puneetdixit200</code></a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/serde-rs/json/commit/de8500740cdcabffb9734f503e4889def823cf10"><code>de85007</code></a>
Release 1.0.151</li>
<li><a
href="https://github.com/serde-rs/json/commit/3b2b3c5f28c20ed988bd081a4147c535e7e65c74"><code>3b2b3c5</code></a>
Merge pull request <a
href="https://redirect.github.com/serde-rs/json/issues/1331">#1331</a>
from WonderLawrence/rawvalue-from-string-unchecked</li>
<li><a
href="https://github.com/serde-rs/json/commit/0406d96860e9d8b9252e2002fa3e626ae48ca1b0"><code>0406d96</code></a>
Debug-assert well-formedness and no-whitespace in
from_string_unchecked</li>
<li><a
href="https://github.com/serde-rs/json/commit/cf16f75d81e28c723323bfc60a68fc02d2994fff"><code>cf16f75</code></a>
Add RawValue::from_string_unchecked</li>
<li><a
href="https://github.com/serde-rs/json/commit/827a315bf2198558f0325b07bcc1e2cd973aba2f"><code>827a315</code></a>
Update actions/upload-artifact@v6 -&gt; v7</li>
<li><a
href="https://github.com/serde-rs/json/commit/cea36a5c017ebffdeb95d0cd0f1aad473bfab758"><code>cea36a5</code></a>
Update actions/checkout@v6 -&gt; v7</li>
<li><a
href="https://github.com/serde-rs/json/commit/a1ae73ac6a6940a4a57c673aebaa13ed4dfe3e8c"><code>a1ae73a</code></a>
Release 1.0.150</li>
<li><a
href="https://github.com/serde-rs/json/commit/1a360b0a6c003912afc3503c834b0edd798bca28"><code>1a360b0</code></a>
Merge pull request <a
href="https://redirect.github.com/serde-rs/json/issues/1324">#1324</a>
from puneetdixit200/reject-non-string-enum-keys</li>
<li><a
href="https://github.com/serde-rs/json/commit/2037b634f9dccbddc11cff189ebeb5854fa0e01c"><code>2037b63</code></a>
Reject non-string enum object keys</li>
<li><a
href="https://github.com/serde-rs/json/commit/5d30df60e916e9b8fc46c74794007ff271fdfbbf"><code>5d30df6</code></a>
Resolve manual_assert_eq pedantic clippy lint</li>
<li>Additional commits viewable in <a
href="https://github.com/serde-rs/json/compare/v1.0.149...v1.0.151">compare
view</a></li>
</ul>
</details>
<br />

Updates `serde` from 1.0.228 to 1.0.229
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/serde-rs/serde/releases">serde's
releases</a>.</em></p>
<blockquote>
<h2>v1.0.229</h2>
<ul>
<li>Update to syn 3</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/serde-rs/serde/commit/7fc3b4c30c94f73a96ebd1553f2b090d928fc3a8"><code>7fc3b4c</code></a>
Release 1.0.229</li>
<li><a
href="https://github.com/serde-rs/serde/commit/6d6e9a11101354ce769a3438a088b6b9305c1863"><code>6d6e9a1</code></a>
Merge pull request <a
href="https://redirect.github.com/serde-rs/serde/issues/3085">#3085</a>
from dtolnay/syn3</li>
<li><a
href="https://github.com/serde-rs/serde/commit/6dec3b751126c8338cac0fe8085612d695e4ecf3"><code>6dec3b7</code></a>
Update to syn 3</li>
<li><a
href="https://github.com/serde-rs/serde/commit/cfe669241065984177ff63af8b45058e6e9b499d"><code>cfe6692</code></a>
Resolve mut_mut pedantic clippy lint</li>
<li><a
href="https://github.com/serde-rs/serde/commit/1023d077510b4aef36a41ef56fdb7798568a2654"><code>1023d07</code></a>
Update actions/upload-artifact@v6 -&gt; v7</li>
<li><a
href="https://github.com/serde-rs/serde/commit/dd682c2c86aa7629e77c1ccd93212d3729f4c66d"><code>dd682c2</code></a>
Update actions/checkout@v6 -&gt; v7</li>
<li><a
href="https://github.com/serde-rs/serde/commit/5f0f18b9211732f2d82f73b5a43e4f5ff3701251"><code>5f0f18b</code></a>
Update ui test suite to nightly-2026-06-01</li>
<li><a
href="https://github.com/serde-rs/serde/commit/63a1498f0e7be991ffac5939bdd202ca16e9a23f"><code>63a1498</code></a>
Regenerate stderr with trybuild normalization fixes</li>
<li><a
href="https://github.com/serde-rs/serde/commit/fa7da4a93567ed347ad0735c28e439fca688ef26"><code>fa7da4a</code></a>
Fix unused_features warning</li>
<li><a
href="https://github.com/serde-rs/serde/commit/6b1a17851ea3d86a56aa116ca1cbf428f8d5f22d"><code>6b1a178</code></a>
Unpin CI miri toolchain</li>
<li>Additional commits viewable in <a
href="https://github.com/serde-rs/serde/compare/v1.0.228...v1.0.229">compare
view</a></li>
</ul>
</details>
<br />

Updates `serde` from 1.0.228 to 1.0.229
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/serde-rs/serde/releases">serde's
releases</a>.</em></p>
<blockquote>
<h2>v1.0.229</h2>
<ul>
<li>Update to syn 3</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/serde-rs/serde/commit/7fc3b4c30c94f73a96ebd1553f2b090d928fc3a8"><code>7fc3b4c</code></a>
Release 1.0.229</li>
<li><a
href="https://github.com/serde-rs/serde/commit/6d6e9a11101354ce769a3438a088b6b9305c1863"><code>6d6e9a1</code></a>
Merge pull request <a
href="https://redirect.github.com/serde-rs/serde/issues/3085">#3085</a>
from dtolnay/syn3</li>
<li><a
href="https://github.com/serde-rs/serde/commit/6dec3b751126c8338cac0fe8085612d695e4ecf3"><code>6dec3b7</code></a>
Update to syn 3</li>
<li><a
href="https://github.com/serde-rs/serde/commit/cfe669241065984177ff63af8b45058e6e9b499d"><code>cfe6692</code></a>
Resolve mut_mut pedantic clippy lint</li>
<li><a
href="https://github.com/serde-rs/serde/commit/1023d077510b4aef36a41ef56fdb7798568a2654"><code>1023d07</code></a>
Update actions/upload-artifact@v6 -&gt; v7</li>
<li><a
href="https://github.com/serde-rs/serde/commit/dd682c2c86aa7629e77c1ccd93212d3729f4c66d"><code>dd682c2</code></a>
Update actions/checkout@v6 -&gt; v7</li>
<li><a
href="https://github.com/serde-rs/serde/commit/5f0f18b9211732f2d82f73b5a43e4f5ff3701251"><code>5f0f18b</code></a>
Update ui test suite to nightly-2026-06-01</li>
<li><a
href="https://github.com/serde-rs/serde/commit/63a1498f0e7be991ffac5939bdd202ca16e9a23f"><code>63a1498</code></a>
Regenerate stderr with trybuild normalization fixes</li>
<li><a
href="https://github.com/serde-rs/serde/commit/fa7da4a93567ed347ad0735c28e439fca688ef26"><code>fa7da4a</code></a>
Fix unused_features warning</li>
<li><a
href="https://github.com/serde-rs/serde/commit/6b1a17851ea3d86a56aa116ca1cbf428f8d5f22d"><code>6b1a178</code></a>
Unpin CI miri toolchain</li>
<li>Additional commits viewable in <a
href="https://github.com/serde-rs/serde/compare/v1.0.228...v1.0.229">compare
view</a></li>
</ul>
</details>
<br />

Updates `serde_json` from 1.0.149 to 1.0.151
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/serde-rs/json/releases">serde_json's
releases</a>.</em></p>
<blockquote>
<h2>v1.0.151</h2>
<ul>
<li>Add RawValue::from_string_unchecked (<a
href="https://redirect.github.com/serde-rs/json/issues/1331">#1331</a>,
thanks <a
href="https://github.com/WonderLawrence"><code>@​WonderLawrence</code></a>)</li>
</ul>
<h2>v1.0.150</h2>
<ul>
<li>Reject non-string enum object keys (<a
href="https://redirect.github.com/serde-rs/json/issues/1324">#1324</a>,
thanks <a
href="https://github.com/puneetdixit200"><code>@​puneetdixit200</code></a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/serde-rs/json/commit/de8500740cdcabffb9734f503e4889def823cf10"><code>de85007</code></a>
Release 1.0.151</li>
<li><a
href="https://github.com/serde-rs/json/commit/3b2b3c5f28c20ed988bd081a4147c535e7e65c74"><code>3b2b3c5</code></a>
Merge pull request <a
href="https://redirect.github.com/serde-rs/json/issues/1331">#1331</a>
from WonderLawrence/rawvalue-from-string-unchecked</li>
<li><a
href="https://github.com/serde-rs/json/commit/0406d96860e9d8b9252e2002fa3e626ae48ca1b0"><code>0406d96</code></a>
Debug-assert well-formedness and no-whitespace in
from_string_unchecked</li>
<li><a
href="https://github.com/serde-rs/json/commit/cf16f75d81e28c723323bfc60a68fc02d2994fff"><code>cf16f75</code></a>
Add RawValue::from_string_unchecked</li>
<li><a
href="https://github.com/serde-rs/json/commit/827a315bf2198558f0325b07bcc1e2cd973aba2f"><code>827a315</code></a>
Update actions/upload-artifact@v6 -&gt; v7</li>
<li><a
href="https://github.com/serde-rs/json/commit/cea36a5c017ebffdeb95d0cd0f1aad473bfab758"><code>cea36a5</code></a>
Update actions/checkout@v6 -&gt; v7</li>
<li><a
href="https://github.com/serde-rs/json/commit/a1ae73ac6a6940a4a57c673aebaa13ed4dfe3e8c"><code>a1ae73a</code></a>
Release 1.0.150</li>
<li><a
href="https://github.com/serde-rs/json/commit/1a360b0a6c003912afc3503c834b0edd798bca28"><code>1a360b0</code></a>
Merge pull request <a
href="https://redirect.github.com/serde-rs/json/issues/1324">#1324</a>
from puneetdixit200/reject-non-string-enum-keys</li>
<li><a
href="https://github.com/serde-rs/json/commit/2037b634f9dccbddc11cff189ebeb5854fa0e01c"><code>2037b63</code></a>
Reject non-string enum object keys</li>
<li><a
href="https://github.com/serde-rs/json/commit/5d30df60e916e9b8fc46c74794007ff271fdfbbf"><code>5d30df6</code></a>
Resolve manual_assert_eq pedantic clippy lint</li>
<li>Additional commits viewable in <a
href="https://github.com/serde-rs/json/compare/v1.0.149...v1.0.151">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-08-01 22:41:56 +01:00
dependabot[bot]andAnthony Stirling 5f2001c708 build(deps-dev): bump brace-expansion from 1.1.14 to 1.1.16 in /frontend (#7149)
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion)
from 1.1.14 to 1.1.16.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/juliangruber/brace-expansion/releases">brace-expansion's
releases</a>.</em></p>
<blockquote>
<h2>v1.1.15</h2>
<ul>
<li>Backport v5.0.6 change to v1 (<a
href="https://redirect.github.com/juliangruber/brace-expansion/issues/111">#111</a>)
0b09384</li>
</ul>
<hr />
<p><a
href="https://github.com/juliangruber/brace-expansion/compare/v1.1.14...v1.1.15">https://github.com/juliangruber/brace-expansion/compare/v1.1.14...v1.1.15</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/juliangruber/brace-expansion/commit/447763a91a613cfa67ac73096cbc1de9a2304f97"><code>447763a</code></a>
1.1.16</li>
<li><a
href="https://github.com/juliangruber/brace-expansion/commit/d74e63030c012e3b7ae81657b8d665619cd51b95"><code>d74e630</code></a>
fix: v1 backport for CVE-2026-13149 (<a
href="https://redirect.github.com/juliangruber/brace-expansion/issues/122">#122</a>)</li>
<li><a
href="https://github.com/juliangruber/brace-expansion/commit/2203f4f4895eba16c4d408b4219ce1b8e5f6ff24"><code>2203f4f</code></a>
1.1.15</li>
<li><a
href="https://github.com/juliangruber/brace-expansion/commit/0b0938410732370559704230724ca4a44d1b29fd"><code>0b09384</code></a>
Backport v5.0.6 change to v1 (<a
href="https://redirect.github.com/juliangruber/brace-expansion/issues/111">#111</a>)</li>
<li>See full diff in <a
href="https://github.com/juliangruber/brace-expansion/compare/v1.1.14...v1.1.16">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=brace-expansion&package-manager=npm_and_yarn&previous-version=1.1.14&new-version=1.1.16)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/Stirling-Tools/Stirling-PDF/network/alerts).

</details>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-08-01 22:41:39 +01:00
dependabot[bot]andAnthony Stirling 18fb663b4d build(deps): bump bouncycastleVersion from 1.84 to 1.85 (#7132)
Bumps `bouncycastleVersion` from 1.84 to 1.85.
Updates `org.bouncycastle:bcprov-jdk18on` from 1.84 to 1.85
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/bcgit/bc-java/blob/main/docs/releasenotes.html">org.bouncycastle:bcprov-jdk18on's
changelog</a>.</em></p>
<blockquote>
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<p><!-- raw HTML omitted --><!-- raw HTML omitted -->2.2.1 Version<!--
raw HTML omitted --><!-- raw HTML omitted -->
Release: 1.85, 1.85.1<!-- raw HTML omitted -->
Date:      2026, July 12th</p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/bcgit/bc-java/commits">compare view</a></li>
</ul>
</details>
<br />

Updates `org.bouncycastle:bcpkix-jdk18on` from 1.84 to 1.85
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/bcgit/bc-java/blob/main/docs/releasenotes.html">org.bouncycastle:bcpkix-jdk18on's
changelog</a>.</em></p>
<blockquote>
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<p><!-- raw HTML omitted --><!-- raw HTML omitted -->2.2.1 Version<!--
raw HTML omitted --><!-- raw HTML omitted -->
Release: 1.85, 1.85.1<!-- raw HTML omitted -->
Date:      2026, July 12th</p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/bcgit/bc-java/commits">compare view</a></li>
</ul>
</details>
<br />

Updates `org.bouncycastle:bcutil-jdk18on` from 1.84 to 1.85
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/bcgit/bc-java/blob/main/docs/releasenotes.html">org.bouncycastle:bcutil-jdk18on's
changelog</a>.</em></p>
<blockquote>
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<p><!-- raw HTML omitted --><!-- raw HTML omitted -->2.2.1 Version<!--
raw HTML omitted --><!-- raw HTML omitted -->
Release: 1.85, 1.85.1<!-- raw HTML omitted -->
Date:      2026, July 12th</p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/bcgit/bc-java/commits">compare view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-08-01 22:41:26 +01:00
dependabot[bot]andLudy 73668bcae8 build(deps): bump the mui group across 1 directory with 2 updates (#6497)
Bumps the mui group with 1 update in the /frontend directory:
[@mui/icons-material](https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material).

Updates `@mui/icons-material` from 9.0.0 to 9.2.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/mui/material-ui/releases">@​mui/icons-material's
releases</a>.</em></p>
<blockquote>
<h2>v9.2.0</h2>
<p>A big thanks to the 9 contributors who made this release
possible.</p>
<ul>
<li>⚙️ Add support for <a
href="https://mui.com/material-ui/guides/typescript/#allowing-data-attributes-on-slotprops"><code>data-*</code>
attributes on <code>slotProps</code></a>.</li>
</ul>
<h3><code>@mui/material@9.1.3</code></h3>
<ul>
<li>[l10n] Add missing MuiPagination localization to zh-CN locale (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48741">#48741</a>)
<a
href="https://github.com/greymoth-jp"><code>@​greymoth-jp</code></a></li>
<li>[select] Guard display ref during mouse down (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48744">#48744</a>)
<a
href="https://github.com/michelengelen"><code>@​michelengelen</code></a></li>
</ul>
<h3><code>@mui/utils@9.1.3</code></h3>
<ul>
<li>[utils] Add opt-in <code>DataAttributesOverrides</code> augmentation
for slot props (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48554">#48554</a>)
<a href="https://github.com/LukasTy"><code>@​LukasTy</code></a></li>
</ul>
<h3>Docs</h3>
<ul>
<li>[docs] Improve Icon Dialog responsiveness on small screens (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48639">#48639</a>)
<a
href="https://github.com/Prakash1185"><code>@​Prakash1185</code></a></li>
<li>[docs] Fix invalid UTF-8 in skill references (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48739">#48739</a>)
<a href="https://github.com/mturac"><code>@​mturac</code></a></li>
</ul>
<h3>Core</h3>
<ul>
<li>[code-infra] Resolve Renovate dashboard warnings (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48700">#48700</a>)
<a
href="https://github.com/Sushantplive"><code>@​Sushantplive</code></a></li>
<li>[code-infra] Validate npm publishing through dry run (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48691">#48691</a>)
<a
href="https://github.com/brijeshb42"><code>@​brijeshb42</code></a></li>
<li>[code-infra] Run prettier after renovate update (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48754">#48754</a>)
<a href="https://github.com/Janpot"><code>@​Janpot</code></a></li>
<li>[code-infra] Fix 'A11y results committed?' check on react-pinned
nightly jobs (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48740">#48740</a>)
<a href="https://github.com/Janpot"><code>@​Janpot</code></a></li>
<li>[core] Remove leftover Joy UI references (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48719">#48719</a>)
<a
href="https://github.com/siriwatknp"><code>@​siriwatknp</code></a></li>
<li>[code-infra] Bump react-router to 7.15.1 (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48725">#48725</a>)
<a href="https://github.com/Janpot"><code>@​Janpot</code></a></li>
<li>[docs-infra] Drive docs analytics IDs via ANALYTICS_ENV (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48694">#48694</a>)
<a href="https://github.com/Janpot"><code>@​Janpot</code></a></li>
<li>[docs-infra] Pre-render API page descriptions (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48693">#48693</a>)
<a
href="https://github.com/brijeshb42"><code>@​brijeshb42</code></a></li>
<li>[code-infra][icons-material] Build lib/package.json with code-infra
--no-expand (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48689">#48689</a>)
<a href="https://github.com/Janpot"><code>@​Janpot</code></a></li>
<li>[code-infra] Fix react@18/next nightly workflow (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48635">#48635</a>)
<a href="https://github.com/Janpot"><code>@​Janpot</code></a></li>
</ul>
<p>All contributors of this release in alphabetical order: <a
href="https://github.com/brijeshb42"><code>@​brijeshb42</code></a>, <a
href="https://github.com/greymoth-jp"><code>@​greymoth-jp</code></a>, <a
href="https://github.com/Janpot"><code>@​Janpot</code></a>, <a
href="https://github.com/LukasTy"><code>@​LukasTy</code></a>, <a
href="https://github.com/michelengelen"><code>@​michelengelen</code></a>,
<a href="https://github.com/mturac"><code>@​mturac</code></a>, <a
href="https://github.com/Prakash1185"><code>@​Prakash1185</code></a>, <a
href="https://github.com/siriwatknp"><code>@​siriwatknp</code></a>, <a
href="https://github.com/Sushantplive"><code>@​Sushantplive</code></a></p>
<h2>v9.1.2</h2>
<p>A big thanks to the 5 contributors who made this release
possible.</p>
<h3><code>@mui/material@9.1.2</code></h3>
<ul>
<li>[autocomplete] Don't submit forms when committing
<code>freeSolo</code> value with Enter key (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48679">#48679</a>)
<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a></li>
<li>[transitions] Fix RTG import in ESM (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48645">#48645</a>)
<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a></li>
</ul>
<h3><code>@mui/system@9.1.2</code></h3>
<ul>
<li>[InitColorSchemeScript] Fix script tag warning in Next.js 16 dev
mode (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48671">#48671</a>)
<a
href="https://github.com/siriwatknp"><code>@​siriwatknp</code></a></li>
</ul>
<h3>Docs</h3>
<ul>
<li>Fix typos in release instructions (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48687">#48687</a>)
<a
href="https://github.com/brijeshb42"><code>@​brijeshb42</code></a></li>
<li>Update <code>@​mui/x</code>-* packages to latest (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48661">#48661</a>)
<a href="https://github.com/Janpot"><code>@​Janpot</code></a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/mui/material-ui/blob/master/CHANGELOG.md">@​mui/icons-material's
changelog</a>.</em></p>
<blockquote>
<h2>9.2.0</h2>
<!-- raw HTML omitted -->
<p><em>Jul 3, 2026</em></p>
<p>A big thanks to the 9 contributors who made this release
possible.</p>
<ul>
<li>⚙️ Add support for <a
href="https://mui.com/material-ui/guides/typescript/#allowing-data-attributes-on-slotprops"><code>data-*</code>
attributes on <code>slotProps</code></a>.</li>
</ul>
<h3><code>@mui/material@9.2.0</code></h3>
<ul>
<li>[l10n] Add missing MuiPagination localization to zh-CN locale (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48741">#48741</a>)
<a
href="https://github.com/greymoth-jp"><code>@​greymoth-jp</code></a></li>
<li>[select] Guard display ref during mouse down (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48744">#48744</a>)
<a
href="https://github.com/michelengelen"><code>@​michelengelen</code></a></li>
</ul>
<h3><code>@mui/utils@9.2.0</code></h3>
<ul>
<li>[utils] Add opt-in <code>DataAttributesOverrides</code> augmentation
for slot props (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48554">#48554</a>)
<a href="https://github.com/LukasTy"><code>@​LukasTy</code></a></li>
</ul>
<h3>Docs</h3>
<ul>
<li>[docs] Improve Icon Dialog responsiveness on small screens (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48639">#48639</a>)
<a
href="https://github.com/Prakash1185"><code>@​Prakash1185</code></a></li>
<li>[docs] Fix invalid UTF-8 in skill references (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48739">#48739</a>)
<a href="https://github.com/mturac"><code>@​mturac</code></a></li>
</ul>
<h3>Core</h3>
<ul>
<li>[code-infra] Resolve Renovate dashboard warnings (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48700">#48700</a>)
<a
href="https://github.com/Sushantplive"><code>@​Sushantplive</code></a></li>
<li>[code-infra] Validate npm publishing through dry run (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48691">#48691</a>)
<a
href="https://github.com/brijeshb42"><code>@​brijeshb42</code></a></li>
<li>[code-infra] Run prettier after renovate update (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48754">#48754</a>)
<a href="https://github.com/Janpot"><code>@​Janpot</code></a></li>
<li>[code-infra] Fix 'A11y results committed?' check on react-pinned
nightly jobs (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48740">#48740</a>)
<a href="https://github.com/Janpot"><code>@​Janpot</code></a></li>
<li>[core] Remove leftover Joy UI references (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48719">#48719</a>)
<a
href="https://github.com/siriwatknp"><code>@​siriwatknp</code></a></li>
<li>[code-infra] Bump react-router to 7.15.1 (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48725">#48725</a>)
<a href="https://github.com/Janpot"><code>@​Janpot</code></a></li>
<li>[docs-infra] Drive docs analytics IDs via ANALYTICS_ENV (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48694">#48694</a>)
<a href="https://github.com/Janpot"><code>@​Janpot</code></a></li>
<li>[docs-infra] Pre-render API page descriptions (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48693">#48693</a>)
<a
href="https://github.com/brijeshb42"><code>@​brijeshb42</code></a></li>
<li>[code-infra][icons-material] Build lib/package.json with code-infra
--no-expand (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48689">#48689</a>)
<a href="https://github.com/Janpot"><code>@​Janpot</code></a></li>
<li>[code-infra] Fix react@18/next nightly workflow (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48635">#48635</a>)
<a href="https://github.com/Janpot"><code>@​Janpot</code></a></li>
</ul>
<p>All contributors of this release in alphabetical order: <a
href="https://github.com/brijeshb42"><code>@​brijeshb42</code></a>, <a
href="https://github.com/greymoth-jp"><code>@​greymoth-jp</code></a>, <a
href="https://github.com/Janpot"><code>@​Janpot</code></a>, <a
href="https://github.com/LukasTy"><code>@​LukasTy</code></a>, <a
href="https://github.com/michelengelen"><code>@​michelengelen</code></a>,
<a href="https://github.com/mturac"><code>@​mturac</code></a>, <a
href="https://github.com/Prakash1185"><code>@​Prakash1185</code></a>, <a
href="https://github.com/siriwatknp"><code>@​siriwatknp</code></a>, <a
href="https://github.com/Sushantplive"><code>@​Sushantplive</code></a></p>
<h2>9.1.2</h2>
<!-- raw HTML omitted -->
<p><em>Jun 23, 2026</em></p>
<p>A big thanks to the 5 contributors who made this release
possible.</p>
<h3><code>@mui/material@9.1.2</code></h3>
<ul>
<li>[autocomplete] Don't submit forms when committing
<code>freeSolo</code> value with Enter key (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48679">#48679</a>)
<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/mui/material-ui/commit/cb77df2fdf6b070cd3958af0ffba11e11454bf98"><code>cb77df2</code></a>
[release] v9.2.0 (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48755">#48755</a>)</li>
<li><a
href="https://github.com/mui/material-ui/commit/4206a995e11373a82f0fa29211434f910fa62d98"><code>4206a99</code></a>
[icons-material] Build lib/package.json with code-infra --no-expand (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48689">#48689</a>)</li>
<li><a
href="https://github.com/mui/material-ui/commit/0f9b43f4e001102e9aa94f10c3882e15277908a5"><code>0f9b43f</code></a>
[release] v9.1.1 (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48656">#48656</a>)</li>
<li><a
href="https://github.com/mui/material-ui/commit/ef607d7d9254fb3bebbf739f9e9217b086381440"><code>ef607d7</code></a>
[code-infra] Port codebase to use tsgo (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48616">#48616</a>)</li>
<li><a
href="https://github.com/mui/material-ui/commit/a389a2d9bc2a89c6c86c4d98aca51c3fa809a753"><code>a389a2d</code></a>
[release] v9.1.0 (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48620">#48620</a>)</li>
<li><a
href="https://github.com/mui/material-ui/commit/a34a94d094338213d7e3aba73ee85c9deaf2dafa"><code>a34a94d</code></a>
[docs] Use the standard license header preferred by OSI and GitHub</li>
<li><a
href="https://github.com/mui/material-ui/commit/a11407d18d3c241657e623e9f626bdd803e4ec79"><code>a11407d</code></a>
Bump react monorepo to 19.2.6 (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48511">#48511</a>)</li>
<li><a
href="https://github.com/mui/material-ui/commit/ea0f8b93c7936c057c17d4c648a59920f5382090"><code>ea0f8b9</code></a>
Bump code-infra:devDependencies (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48401">#48401</a>)</li>
<li><a
href="https://github.com/mui/material-ui/commit/35b5b62f1861ec14b0a4b4c7aed8a9304f3e30e4"><code>35b5b62</code></a>
Bump react monorepo to 19.2.5 (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48406">#48406</a>)</li>
<li><a
href="https://github.com/mui/material-ui/commit/933bdf67f2405b9bfd4a115b957bc60fd2abaccf"><code>933bdf6</code></a>
v9.0.1 (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48479">#48479</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/mui/material-ui/commits/v9.2.0/packages/mui-icons-material">compare
view</a></li>
</ul>
</details>
<br />

Updates `@mui/material` from 9.0.0 to 9.2.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/mui/material-ui/releases">@​mui/material's
releases</a>.</em></p>
<blockquote>
<h2>v9.2.0</h2>
<p>A big thanks to the 9 contributors who made this release
possible.</p>
<ul>
<li>⚙️ Add support for <a
href="https://mui.com/material-ui/guides/typescript/#allowing-data-attributes-on-slotprops"><code>data-*</code>
attributes on <code>slotProps</code></a>.</li>
</ul>
<h3><code>@mui/material@9.1.3</code></h3>
<ul>
<li>[l10n] Add missing MuiPagination localization to zh-CN locale (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48741">#48741</a>)
<a
href="https://github.com/greymoth-jp"><code>@​greymoth-jp</code></a></li>
<li>[select] Guard display ref during mouse down (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48744">#48744</a>)
<a
href="https://github.com/michelengelen"><code>@​michelengelen</code></a></li>
</ul>
<h3><code>@mui/utils@9.1.3</code></h3>
<ul>
<li>[utils] Add opt-in <code>DataAttributesOverrides</code> augmentation
for slot props (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48554">#48554</a>)
<a href="https://github.com/LukasTy"><code>@​LukasTy</code></a></li>
</ul>
<h3>Docs</h3>
<ul>
<li>[docs] Improve Icon Dialog responsiveness on small screens (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48639">#48639</a>)
<a
href="https://github.com/Prakash1185"><code>@​Prakash1185</code></a></li>
<li>[docs] Fix invalid UTF-8 in skill references (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48739">#48739</a>)
<a href="https://github.com/mturac"><code>@​mturac</code></a></li>
</ul>
<h3>Core</h3>
<ul>
<li>[code-infra] Resolve Renovate dashboard warnings (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48700">#48700</a>)
<a
href="https://github.com/Sushantplive"><code>@​Sushantplive</code></a></li>
<li>[code-infra] Validate npm publishing through dry run (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48691">#48691</a>)
<a
href="https://github.com/brijeshb42"><code>@​brijeshb42</code></a></li>
<li>[code-infra] Run prettier after renovate update (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48754">#48754</a>)
<a href="https://github.com/Janpot"><code>@​Janpot</code></a></li>
<li>[code-infra] Fix 'A11y results committed?' check on react-pinned
nightly jobs (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48740">#48740</a>)
<a href="https://github.com/Janpot"><code>@​Janpot</code></a></li>
<li>[core] Remove leftover Joy UI references (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48719">#48719</a>)
<a
href="https://github.com/siriwatknp"><code>@​siriwatknp</code></a></li>
<li>[code-infra] Bump react-router to 7.15.1 (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48725">#48725</a>)
<a href="https://github.com/Janpot"><code>@​Janpot</code></a></li>
<li>[docs-infra] Drive docs analytics IDs via ANALYTICS_ENV (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48694">#48694</a>)
<a href="https://github.com/Janpot"><code>@​Janpot</code></a></li>
<li>[docs-infra] Pre-render API page descriptions (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48693">#48693</a>)
<a
href="https://github.com/brijeshb42"><code>@​brijeshb42</code></a></li>
<li>[code-infra][icons-material] Build lib/package.json with code-infra
--no-expand (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48689">#48689</a>)
<a href="https://github.com/Janpot"><code>@​Janpot</code></a></li>
<li>[code-infra] Fix react@18/next nightly workflow (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48635">#48635</a>)
<a href="https://github.com/Janpot"><code>@​Janpot</code></a></li>
</ul>
<p>All contributors of this release in alphabetical order: <a
href="https://github.com/brijeshb42"><code>@​brijeshb42</code></a>, <a
href="https://github.com/greymoth-jp"><code>@​greymoth-jp</code></a>, <a
href="https://github.com/Janpot"><code>@​Janpot</code></a>, <a
href="https://github.com/LukasTy"><code>@​LukasTy</code></a>, <a
href="https://github.com/michelengelen"><code>@​michelengelen</code></a>,
<a href="https://github.com/mturac"><code>@​mturac</code></a>, <a
href="https://github.com/Prakash1185"><code>@​Prakash1185</code></a>, <a
href="https://github.com/siriwatknp"><code>@​siriwatknp</code></a>, <a
href="https://github.com/Sushantplive"><code>@​Sushantplive</code></a></p>
<h2>v9.1.2</h2>
<p>A big thanks to the 5 contributors who made this release
possible.</p>
<h3><code>@mui/material@9.1.2</code></h3>
<ul>
<li>[autocomplete] Don't submit forms when committing
<code>freeSolo</code> value with Enter key (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48679">#48679</a>)
<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a></li>
<li>[transitions] Fix RTG import in ESM (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48645">#48645</a>)
<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a></li>
</ul>
<h3><code>@mui/system@9.1.2</code></h3>
<ul>
<li>[InitColorSchemeScript] Fix script tag warning in Next.js 16 dev
mode (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48671">#48671</a>)
<a
href="https://github.com/siriwatknp"><code>@​siriwatknp</code></a></li>
</ul>
<h3>Docs</h3>
<ul>
<li>Fix typos in release instructions (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48687">#48687</a>)
<a
href="https://github.com/brijeshb42"><code>@​brijeshb42</code></a></li>
<li>Update <code>@​mui/x</code>-* packages to latest (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48661">#48661</a>)
<a href="https://github.com/Janpot"><code>@​Janpot</code></a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/mui/material-ui/blob/master/CHANGELOG.md">@​mui/material's
changelog</a>.</em></p>
<blockquote>
<h2>9.2.0</h2>
<!-- raw HTML omitted -->
<p><em>Jul 3, 2026</em></p>
<p>A big thanks to the 9 contributors who made this release
possible.</p>
<ul>
<li>⚙️ Add support for <a
href="https://mui.com/material-ui/guides/typescript/#allowing-data-attributes-on-slotprops"><code>data-*</code>
attributes on <code>slotProps</code></a>.</li>
</ul>
<h3><code>@mui/material@9.2.0</code></h3>
<ul>
<li>[l10n] Add missing MuiPagination localization to zh-CN locale (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48741">#48741</a>)
<a
href="https://github.com/greymoth-jp"><code>@​greymoth-jp</code></a></li>
<li>[select] Guard display ref during mouse down (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48744">#48744</a>)
<a
href="https://github.com/michelengelen"><code>@​michelengelen</code></a></li>
</ul>
<h3><code>@mui/utils@9.2.0</code></h3>
<ul>
<li>[utils] Add opt-in <code>DataAttributesOverrides</code> augmentation
for slot props (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48554">#48554</a>)
<a href="https://github.com/LukasTy"><code>@​LukasTy</code></a></li>
</ul>
<h3>Docs</h3>
<ul>
<li>[docs] Improve Icon Dialog responsiveness on small screens (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48639">#48639</a>)
<a
href="https://github.com/Prakash1185"><code>@​Prakash1185</code></a></li>
<li>[docs] Fix invalid UTF-8 in skill references (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48739">#48739</a>)
<a href="https://github.com/mturac"><code>@​mturac</code></a></li>
</ul>
<h3>Core</h3>
<ul>
<li>[code-infra] Resolve Renovate dashboard warnings (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48700">#48700</a>)
<a
href="https://github.com/Sushantplive"><code>@​Sushantplive</code></a></li>
<li>[code-infra] Validate npm publishing through dry run (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48691">#48691</a>)
<a
href="https://github.com/brijeshb42"><code>@​brijeshb42</code></a></li>
<li>[code-infra] Run prettier after renovate update (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48754">#48754</a>)
<a href="https://github.com/Janpot"><code>@​Janpot</code></a></li>
<li>[code-infra] Fix 'A11y results committed?' check on react-pinned
nightly jobs (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48740">#48740</a>)
<a href="https://github.com/Janpot"><code>@​Janpot</code></a></li>
<li>[core] Remove leftover Joy UI references (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48719">#48719</a>)
<a
href="https://github.com/siriwatknp"><code>@​siriwatknp</code></a></li>
<li>[code-infra] Bump react-router to 7.15.1 (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48725">#48725</a>)
<a href="https://github.com/Janpot"><code>@​Janpot</code></a></li>
<li>[docs-infra] Drive docs analytics IDs via ANALYTICS_ENV (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48694">#48694</a>)
<a href="https://github.com/Janpot"><code>@​Janpot</code></a></li>
<li>[docs-infra] Pre-render API page descriptions (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48693">#48693</a>)
<a
href="https://github.com/brijeshb42"><code>@​brijeshb42</code></a></li>
<li>[code-infra][icons-material] Build lib/package.json with code-infra
--no-expand (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48689">#48689</a>)
<a href="https://github.com/Janpot"><code>@​Janpot</code></a></li>
<li>[code-infra] Fix react@18/next nightly workflow (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48635">#48635</a>)
<a href="https://github.com/Janpot"><code>@​Janpot</code></a></li>
</ul>
<p>All contributors of this release in alphabetical order: <a
href="https://github.com/brijeshb42"><code>@​brijeshb42</code></a>, <a
href="https://github.com/greymoth-jp"><code>@​greymoth-jp</code></a>, <a
href="https://github.com/Janpot"><code>@​Janpot</code></a>, <a
href="https://github.com/LukasTy"><code>@​LukasTy</code></a>, <a
href="https://github.com/michelengelen"><code>@​michelengelen</code></a>,
<a href="https://github.com/mturac"><code>@​mturac</code></a>, <a
href="https://github.com/Prakash1185"><code>@​Prakash1185</code></a>, <a
href="https://github.com/siriwatknp"><code>@​siriwatknp</code></a>, <a
href="https://github.com/Sushantplive"><code>@​Sushantplive</code></a></p>
<h2>9.1.2</h2>
<!-- raw HTML omitted -->
<p><em>Jun 23, 2026</em></p>
<p>A big thanks to the 5 contributors who made this release
possible.</p>
<h3><code>@mui/material@9.1.2</code></h3>
<ul>
<li>[autocomplete] Don't submit forms when committing
<code>freeSolo</code> value with Enter key (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48679">#48679</a>)
<a
href="https://github.com/mj12albert"><code>@​mj12albert</code></a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/mui/material-ui/commit/cb77df2fdf6b070cd3958af0ffba11e11454bf98"><code>cb77df2</code></a>
[release] v9.2.0 (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48755">#48755</a>)</li>
<li><a
href="https://github.com/mui/material-ui/commit/ff92ccd23f7dde356a6f17e52f18747647e9a359"><code>ff92ccd</code></a>
[l10n] Add missing MuiPagination localization to zh-CN locale (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48741">#48741</a>)</li>
<li><a
href="https://github.com/mui/material-ui/commit/71f1a967fa83054ef2c8ef05201ac92ff78af860"><code>71f1a96</code></a>
[Select] Guard display ref during mouse down (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48744">#48744</a>)</li>
<li><a
href="https://github.com/mui/material-ui/commit/3ccff759bd1cd8c39c5ee1599a42d9ca682eed7f"><code>3ccff75</code></a>
[internal] Fix outdated links to the React repository</li>
<li><a
href="https://github.com/mui/material-ui/commit/33be3ca2326a1043cdc19b7dce4bd07827555533"><code>33be3ca</code></a>
[test] Fix react@18/next nightly workflow (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48635">#48635</a>)</li>
<li><a
href="https://github.com/mui/material-ui/commit/48a4f98e257c00772fd8de194ec546daff8b5db9"><code>48a4f98</code></a>
[utils] Add opt-in <code>DataAttributesOverrides</code> augmentation for
slot props (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48">#48</a>...</li>
<li><a
href="https://github.com/mui/material-ui/commit/0cc3e24327ab6ef1aab39687cdb074dd692cfef3"><code>0cc3e24</code></a>
[core] Remove leftover Joy UI references (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48719">#48719</a>)</li>
<li><a
href="https://github.com/mui/material-ui/commit/f33d58f7b86d78458216427b91682b3e7bcc5c6a"><code>f33d58f</code></a>
[core] Bump react-router to 7.15.1 (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48725">#48725</a>)</li>
<li><a
href="https://github.com/mui/material-ui/commit/a1ea5d8bb0fedc4dfe26886ab4012771b8f9aae7"><code>a1ea5d8</code></a>
[release] v9.1.2 (<a
href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48713">#48713</a>)</li>
<li><a
href="https://github.com/mui/material-ui/commit/e6a054d39fe8c1f8f877b993aa3f2d88bf1b2d51"><code>e6a054d</code></a>
[autocomplete] Don't submit forms when committing <code>freeSolo</code>
value with Enter...</li>
<li>Additional commits viewable in <a
href="https://github.com/mui/material-ui/commits/v9.2.0/packages/mui-material">compare
view</a></li>
</ul>
</details>
<br />


> **Note**
> Automatic rebases have been disabled on this pull request as it has
been open for over 30 days.

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ludy <Ludy87@users.noreply.github.com>
2026-08-01 18:52:48 +01:00
dependabot[bot]andAnthony Stirling 80501e599c build(deps): bump windows from 0.61.3 to 0.62.2 in /frontend/editor/src-tauri (#6419)
Bumps [windows](https://github.com/microsoft/windows-rs) from 0.61.3 to
0.62.2.
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/microsoft/windows-rs/commits">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=windows&package-manager=cargo&previous-version=0.61.3&new-version=0.62.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-08-01 18:52:32 +01:00
dependabot[bot] a2ff8f8c8c build(deps): bump step-security/harden-runner from 2.19.3 to 2.20.0 (#7066)
Bumps
[step-security/harden-runner](https://github.com/step-security/harden-runner)
from 2.19.3 to 2.20.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/step-security/harden-runner/releases">step-security/harden-runner's
releases</a>.</em></p>
<blockquote>
<h2>v2.20.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Support for block policy for MacOS and Windows GitHub-hosted
runners</li>
<li>Support for Bitrise MacOS GitHub Actions runners</li>
<li>HTTPS monitoring support for Bun for Linux runners (enterprise
tier)</li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/step-security/harden-runner/compare/v2.19.4...v2.20.0">https://github.com/step-security/harden-runner/compare/v2.19.4...v2.20.0</a></p>
<h2>v2.19.4</h2>
<h2>What's Changed</h2>
<ul>
<li>Improvements for HTTPS Monitoring for the Enterprise tier of Harden
Runner</li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/step-security/harden-runner/compare/v2.19.3...v2.19.4">https://github.com/step-security/harden-runner/compare/v2.19.3...v2.19.4</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/step-security/harden-runner/commit/bf7454d06d71f1098171f2acdf0cd4708d7b5920"><code>bf7454d</code></a>
Merge pull request <a
href="https://redirect.github.com/step-security/harden-runner/issues/673">#673</a>
from step-security/fix/aggregate-error-startup-hang</li>
<li><a
href="https://github.com/step-security/harden-runner/commit/1188420976b49762617c32cc010cba50a9fd7a71"><code>1188420</code></a>
Update non-TLS agent to v0.16.2</li>
<li><a
href="https://github.com/step-security/harden-runner/commit/162cfeac170141192dc6d57ade86ddf59448ad96"><code>162cfea</code></a>
Update non-TLS agent to v0.16.1</li>
<li><a
href="https://github.com/step-security/harden-runner/commit/eb9e1f4943b602b6f338f6f79468e812c2c6b320"><code>eb9e1f4</code></a>
Bring macOS runner updates from PR 674</li>
<li><a
href="https://github.com/step-security/harden-runner/commit/1a10b01783c147498a6dee4fa4e7122325762720"><code>1a10b01</code></a>
Update Windows agent to v1.0.7</li>
<li><a
href="https://github.com/step-security/harden-runner/commit/8b4a105ef5119b20c97c1566b0275b9399ae188d"><code>8b4a105</code></a>
Apply npm audit fixes with release-age cooldown</li>
<li><a
href="https://github.com/step-security/harden-runner/commit/3626e0327723bef1c4e6b01750518eccd380a5df"><code>3626e03</code></a>
Default TLS status check failures to enabled</li>
<li><a
href="https://github.com/step-security/harden-runner/commit/100e08b39cfd419c292df7becc379b0305ac0628"><code>100e08b</code></a>
Update agent-ebpf to v1.8.12</li>
<li><a
href="https://github.com/step-security/harden-runner/commit/774f75f2c6334606d2d3d910a663f93c9ea49b3b"><code>774f75f</code></a>
Update agent to v1.8.9</li>
<li><a
href="https://github.com/step-security/harden-runner/commit/f312657a64c745fae39c2c66cc7c7f7bc4c804d8"><code>f312657</code></a>
Extend missing-agent-dir guard to Linux and macOS cleanup paths</li>
<li>Additional commits viewable in <a
href="https://github.com/step-security/harden-runner/compare/ab7a9404c0f3da075243ca237b5fac12c98deaa5...bf7454d06d71f1098171f2acdf0cd4708d7b5920">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-01 18:52:12 +01:00
Anthony Stirling 547dce4cf3 Add multi-node cluster regression suite (compose stack + behave e2e) (#7026)
# Description of Changes

- The multi-node compose stack + behave suite (11 features)
- The nightly multinode-e2e job in build-enterprise.yml


cuke features are

cluster_health - both nodes boot healthy and join the Valkey backplane
load_balancing - traffic spreads across nodes; no spurious 401 when
bounced
cross_node_auth - a token from one node validates on all nodes (shared
DB keys)
shared_state - teams/sources/org visible from every node
policy_management - create/rename/delete a policy on any node, reflected
everywhere
source_management - source CRUD cross-node; referenced source can't be
deleted anywhere
connections - S3 connection resolves (secret masked) and deletes
cluster-wide
processor_ledger - files processed exactly once even when both nodes
trigger together
policy_run_coordination - a run on one node is visible from every node
rate_limiting - rate-limit counters shared via Valkey, not per node
failover - LB keeps serving when a node dies; recovered node accepts
existing tokens


can now start a full node system with 
export PREMIUM_KEY=<your licence key> ./start-multinode-test.sh
starts a 40 person org DB install with multi node and database
(--no-seed to have without DB on startup)
4 teams
1 s3 connection
1 policy

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have run `task check` to verify linters, typechecks, and tests
pass
- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing)
for more details.
2026-08-01 17:09:11 +01:00
dependabot[bot] aba9275ea7 build(deps): bump postcss from 8.5.12 to 8.5.25 in /devTools (#7232)
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.12 to
8.5.25.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/postcss/postcss/releases">postcss's
releases</a>.</em></p>
<blockquote>
<h2>8.5.25</h2>
<ul>
<li>Fixed 8.5.17 visitor regression.</li>
<li>Fixed <code>list.split()</code> for non-string values (by <a
href="https://github.com/amir-rezaei"><code>@​amir-rezaei</code></a>).</li>
</ul>
<h2>8.5.24</h2>
<ul>
<li>Preserve the BOM after the processing (by <a
href="https://github.com/hdimer"><code>@​hdimer</code></a>).</li>
</ul>
<h2>8.5.23</h2>
<ul>
<li>Do not load source map without <code>opts.from</code> for security
reasons.</li>
</ul>
<h2>8.5.22</h2>
<ul>
<li>Fixed custom property losing semicolon before a comment (by <a
href="https://github.com/sarathfrancis90"><code>@​sarathfrancis90</code></a>).</li>
</ul>
<h2>8.5.21</h2>
<ul>
<li>Fixed childless at-rule losing semicolon before comment (by <a
href="https://github.com/sarathfrancis90"><code>@​sarathfrancis90</code></a>).</li>
<li>Fixed docs (by <a
href="https://github.com/isker"><code>@​isker</code></a>).</li>
</ul>
<h2>8.5.20</h2>
<ul>
<li>Fixed missing space if <code>AtRule#params</code> is set after (by
<a
href="https://github.com/sarathfrancis90"><code>@​sarathfrancis90</code></a>).</li>
<li>Fixed mixing AST error on warnings (by <a
href="https://github.com/MahinAnowar"><code>@​MahinAnowar</code></a>).</li>
</ul>
<h2>8.5.19</h2>
<ul>
<li>Fixed cleaning <code>before</code> for new nodes inserted to
<code>Root</code> (by <a
href="https://github.com/MahinAnowar"><code>@​MahinAnowar</code></a>).</li>
</ul>
<h2>8.5.18</h2>
<ul>
<li>Restricted loading previous source maps file to the
<code>opts.from</code> folder for security reasons (use <code>unsafeMap:
true</code> to disable the check).</li>
</ul>
<h2>8.5.17</h2>
<ul>
<li>Fixed <code>Maximum call stack size exceeded</code> error.</li>
<li>Fixed Prototype hijacking for <code>postcss.fromJSON()</code>.</li>
<li>Fixed <code>Input#origin()</code> for unmapped end position (by <a
href="https://github.com/chatman-media"><code>@​chatman-media</code></a>).</li>
</ul>
<h2>8.5.16</h2>
<ul>
<li>Fixed <code>Input#origin()</code> position (by <a
href="https://github.com/mizdra"><code>@​mizdra</code></a>).</li>
<li>Fixed <code>raws</code> after rehydrating a JSON AST (by <a
href="https://github.com/sarathfrancis90"><code>@​sarathfrancis90</code></a>).</li>
<li>Fixed putting parent-less node in <code>nodes</code> of new node (by
<a
href="https://github.com/MahinAnowar"><code>@​MahinAnowar</code></a>).</li>
<li>Fixed computing <code>offset</code> in <code>positionBy()</code> (by
<a
href="https://github.com/greymoth-jp"><code>@​greymoth-jp</code></a>).</li>
<li>Fixed <code>rangeBy()</code> on <code>index: 0</code> (by <a
href="https://github.com/sarathfrancis90"><code>@​sarathfrancis90</code></a>).</li>
</ul>
<h2>8.5.15</h2>
<ul>
<li>Fixed declaration parsing performance (by <a
href="https://github.com/homanp"><code>@​homanp</code></a>).</li>
</ul>
<h2>8.5.14</h2>
<ul>
<li>Fixed custom syntax regression (by <a
href="https://github.com/43081j"><code>@​43081j</code></a>).</li>
</ul>
<h2>8.5.13</h2>
<ul>
<li>Fixed <code>postcss-scss</code> commend regression.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/postcss/postcss/blob/main/CHANGELOG.md">postcss's
changelog</a>.</em></p>
<blockquote>
<h2>8.5.25</h2>
<ul>
<li>Fixed 8.5.17 visitor regression.</li>
<li>Fixed <code>list.split()</code> for non-string values (by <a
href="https://github.com/amir-rezaei"><code>@​amir-rezaei</code></a>).</li>
</ul>
<h2>8.5.24</h2>
<ul>
<li>Preserve the BOM after the processing (by <a
href="https://github.com/hdimer"><code>@​hdimer</code></a>).</li>
</ul>
<h2>8.5.23</h2>
<ul>
<li>Do not load source map without <code>opts.from</code> for security
reasons.</li>
</ul>
<h2>8.5.22</h2>
<ul>
<li>Fixed custom property losing semicolon before a comment (by <a
href="https://github.com/sarathfrancis90"><code>@​sarathfrancis90</code></a>).</li>
</ul>
<h2>8.5.21</h2>
<ul>
<li>Fixed childless at-rule losing semicolon before comment (by <a
href="https://github.com/sarathfrancis90"><code>@​sarathfrancis90</code></a>).</li>
<li>Fixed docs (by <a
href="https://github.com/isker"><code>@​isker</code></a>).</li>
</ul>
<h2>8.5.20</h2>
<ul>
<li>Fixed missing space if <code>AtRule#params</code> is set after (by
<a
href="https://github.com/sarathfrancis90"><code>@​sarathfrancis90</code></a>).</li>
<li>Fixed mixing AST error on warnings (by <a
href="https://github.com/MahinAnowar"><code>@​MahinAnowar</code></a>).</li>
</ul>
<h2>8.5.19</h2>
<ul>
<li>Fixed cleaning <code>before</code> for new nodes inserted to
<code>Root</code> (by <a
href="https://github.com/MahinAnowar"><code>@​MahinAnowar</code></a>).</li>
</ul>
<h2>8.5.18</h2>
<ul>
<li>Restricted loading previous source maps file to the
<code>opts.from</code> folder for security reasons (use <code>unsafeMap:
true</code> to disable the check).</li>
</ul>
<h2>8.5.17</h2>
<ul>
<li>Fixed <code>Maximum call stack size exceeded</code> error.</li>
<li>Fixed Prototype hijacking for <code>postcss.fromJSON()</code>.</li>
<li>Fixed <code>Input#origin()</code> for unmapped end position (by <a
href="https://github.com/chatman-media"><code>@​chatman-media</code></a>).</li>
</ul>
<h2>8.5.16</h2>
<ul>
<li>Fixed <code>Input#origin()</code> position (by <a
href="https://github.com/mizdra"><code>@​mizdra</code></a>).</li>
<li>Fixed <code>raws</code> after rehydrating a JSON AST (by <a
href="https://github.com/sarathfrancis90"><code>@​sarathfrancis90</code></a>).</li>
<li>Fixed putting parent-less node in <code>nodes</code> of new node (by
<a
href="https://github.com/MahinAnowar"><code>@​MahinAnowar</code></a>).</li>
<li>Fixed computing <code>offset</code> in <code>positionBy()</code> (by
<a
href="https://github.com/greymoth-jp"><code>@​greymoth-jp</code></a>).</li>
<li>Fixed <code>rangeBy()</code> on <code>index: 0</code> (by <a
href="https://github.com/sarathfrancis90"><code>@​sarathfrancis90</code></a>).</li>
</ul>
<h2>8.5.15</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/postcss/postcss/commit/08c989c43cc87edb1ed71408c2f5164c54fc21df"><code>08c989c</code></a>
Release 8.5.25 version</li>
<li><a
href="https://github.com/postcss/postcss/commit/24f681471645cd960ee760ab7f9e348fbabfd42c"><code>24f6814</code></a>
Fix 8.5.17 visitor regression</li>
<li><a
href="https://github.com/postcss/postcss/commit/f2fa53f11daab3a16c7eb8bcaf5a945142341df3"><code>f2fa53f</code></a>
Add supply chain security requirement to PostCSS plugin guide</li>
<li><a
href="https://github.com/postcss/postcss/commit/10edf0b0606f97b1510e040c27bfd078c48d6ea7"><code>10edf0b</code></a>
fix: return empty array for empty string in list.split (<a
href="https://redirect.github.com/postcss/postcss/issues/2121">#2121</a>)</li>
<li><a
href="https://github.com/postcss/postcss/commit/0ebe8ad591621ab4e48311da47a76974617571f9"><code>0ebe8ad</code></a>
Release 8.5.24 version</li>
<li><a
href="https://github.com/postcss/postcss/commit/73218c64245be53e25d58150e0cc7e984f1d162d"><code>73218c6</code></a>
Update dependencies</li>
<li><a
href="https://github.com/postcss/postcss/commit/9a114f62b0deb37be859102f93b414b49385805a"><code>9a114f6</code></a>
Preserve the BOM when stringifying (<a
href="https://redirect.github.com/postcss/postcss/issues/2119">#2119</a>)</li>
<li><a
href="https://github.com/postcss/postcss/commit/90692619125cb9424f5eafd8c64bc76b2da23db1"><code>9069261</code></a>
Fix types check</li>
<li><a
href="https://github.com/postcss/postcss/commit/eb9e1fe793740bb3280bdf5bf98147f857f011bd"><code>eb9e1fe</code></a>
Release 8.5.23 version</li>
<li><a
href="https://github.com/postcss/postcss/commit/9d19c78ac91108b3f7d7130e55c6fa806c0efb84"><code>9d19c78</code></a>
Update dependencies</li>
<li>Additional commits viewable in <a
href="https://github.com/postcss/postcss/compare/8.5.12...8.5.25">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~GitHub%20Actions">GitHub Actions</a>, a new
releaser for postcss since your current version.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=postcss&package-manager=npm_and_yarn&previous-version=8.5.12&new-version=8.5.25)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/Stirling-Tools/Stirling-PDF/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-01 17:08:03 +01:00
dependabot[bot] 71957d5da5 build(deps): bump fast-uri from 3.1.2 to 3.1.5 in /devTools (#7231)
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.2 to
3.1.5.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/fastify/fast-uri/releases">fast-uri's
releases</a>.</em></p>
<blockquote>
<h2>v3.1.5</h2>
<h2>⚠️ Security Warning</h2>
<p>Fix for <a
href="https://github.com/fastify/fast-uri/security/advisories/GHSA-7p8r-x3mc-p8w7">https://github.com/fastify/fast-uri/security/advisories/GHSA-7p8r-x3mc-p8w7</a></p>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/fastify/fast-uri/compare/v3.1.4...v3.1.5">https://github.com/fastify/fast-uri/compare/v3.1.4...v3.1.5</a></p>
<h2>v3.1.4</h2>
<h2>⚠️ Security Release</h2>
<p>Fix for <a
href="https://github.com/fastify/fast-uri/security/advisories/GHSA-v2hh-gcrm-f6hx">https://github.com/fastify/fast-uri/security/advisories/GHSA-v2hh-gcrm-f6hx</a></p>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/fastify/fast-uri/compare/v3.1.3...v3.1.4">https://github.com/fastify/fast-uri/compare/v3.1.3...v3.1.4</a></p>
<h2>v3.1.3</h2>
<h2>⚠️ Security Release</h2>
<ul>
<li>Fixes: <a
href="https://github.com/fastify/fast-uri/security/advisories/GHSA-4c8g-83qw-93j6">https://github.com/fastify/fast-uri/security/advisories/GHSA-4c8g-83qw-93j6</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/fastify/fast-uri/compare/v3.1.2...v3.1.3">https://github.com/fastify/fast-uri/compare/v3.1.2...v3.1.3</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/fastify/fast-uri/commit/5e179cbb4636d5f773ed21126e5bd3068e87e94e"><code>5e179cb</code></a>
Bumped v3.1.5</li>
<li><a
href="https://github.com/fastify/fast-uri/commit/2cad02d6ed428a720499bb7a3c3d6c3d41f10f5a"><code>2cad02d</code></a>
Merge commit from fork</li>
<li><a
href="https://github.com/fastify/fast-uri/commit/6aeece669e4166b2446a89f17c07a3b15dfb7ed4"><code>6aeece6</code></a>
Bumped v3.1.4</li>
<li><a
href="https://github.com/fastify/fast-uri/commit/2d50fbabc80e4d0884fe0f6a98fe118ce6faa353"><code>2d50fba</code></a>
fix: reject literal backslash in URI authority</li>
<li><a
href="https://github.com/fastify/fast-uri/commit/0549fe35b0d482233f3be2816439f3ec803603fa"><code>0549fe3</code></a>
Bumped v3.1.3</li>
<li><a
href="https://github.com/fastify/fast-uri/commit/2a6d357a18a68e6d812824379fd3388a1ae50d05"><code>2a6d357</code></a>
Merge commit from fork</li>
<li>See full diff in <a
href="https://github.com/fastify/fast-uri/compare/v3.1.2...v3.1.5">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=fast-uri&package-manager=npm_and_yarn&previous-version=3.1.2&new-version=3.1.5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/Stirling-Tools/Stirling-PDF/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-01 17:07:56 +01:00
dependabot[bot] 536a9ebaa7 build(deps): bump actions/ai-inference from 2.1.0 to 2.1.1 (#6832)
Bumps [actions/ai-inference](https://github.com/actions/ai-inference)
from 2.1.0 to 2.1.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/ai-inference/releases">actions/ai-inference's
releases</a>.</em></p>
<blockquote>
<h2>v2.1.1</h2>
<h2>What's Changed</h2>
<ul>
<li>fix: delay process.exit on Windows to dodge libuv UV_HANDLE_CLOSING
crash by <a
href="https://github.com/mickey-mikey"><code>@​mickey-mikey</code></a>
in <a
href="https://redirect.github.com/actions/ai-inference/pull/228">actions/ai-inference#228</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/mickey-mikey"><code>@​mickey-mikey</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/ai-inference/pull/228">actions/ai-inference#228</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/ai-inference/compare/v2...v2.1.1">https://github.com/actions/ai-inference/compare/v2...v2.1.1</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/ai-inference/commit/a7805884c80886efc241e94a5351df715968a0ad"><code>a780588</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/ai-inference/issues/228">#228</a>
from mickey-mikey/fix/libuv-windows-exit</li>
<li><a
href="https://github.com/actions/ai-inference/commit/99b696214c2205c9bdca442acae4307252da5067"><code>99b6962</code></a>
chore: npm audit fix — clear 17 transitive-dep CVEs</li>
<li><a
href="https://github.com/actions/ai-inference/commit/200ca3a4231fb2fbf71affa629d3af65e17e3a70"><code>200ca3a</code></a>
ci: disable redundant Biome validators and add dependabot cooldown</li>
<li><a
href="https://github.com/actions/ai-inference/commit/0e2c6977336b824b8dbb490be15874fe6f589e43"><code>0e2c697</code></a>
chore: refresh .licenses cache</li>
<li><a
href="https://github.com/actions/ai-inference/commit/0bb3dae7d8aab92acdb63a4595aa693f6ebc21f8"><code>0bb3dae</code></a>
fix: delay process.exit on Windows to dodge libuv UV_HANDLE_CLOSING
crash</li>
<li>See full diff in <a
href="https://github.com/actions/ai-inference/compare/17ff458cb182449bbb2e43701fcd98f6af8f6570...a7805884c80886efc241e94a5351df715968a0ad">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/ai-inference&package-manager=github_actions&previous-version=2.1.0&new-version=2.1.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

You can trigger a rebase of this PR by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

> **Note**
> Automatic rebases have been disabled on this pull request as it has
been open for over 30 days.

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-01 17:07:32 +01:00
dependabot[bot] c8db0a912e build(deps): bump ubuntu from c4a8d55 to 4fbb8e6 in /docker/unoserver (#6557)
Bumps ubuntu from `c4a8d55` to `4fbb8e6`.


> **Note**
> Automatic rebases have been disabled on this pull request as it has
been open for over 30 days.

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-01 17:07:21 +01:00
dependabot[bot] 88012c6861 build(deps): bump jackson2Version from 2.21.4 to 2.22.1 (#7131)
Bumps `jackson2Version` from 2.21.4 to 2.22.1.
Updates `com.fasterxml.jackson.core:jackson-core` from 2.21.4 to 2.22.1
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/FasterXML/jackson-core/commit/2451603e1931dac6f28ebdbcf308eb24b0f84b26"><code>2451603</code></a>
[maven-release-plugin] prepare release jackson-core-2.22.1</li>
<li><a
href="https://github.com/FasterXML/jackson-core/commit/d3180c0827e45e39c0ca38dc6d650f19aff572ef"><code>d3180c0</code></a>
Prep for 2.22.1 release</li>
<li><a
href="https://github.com/FasterXML/jackson-core/commit/6c5bae8184485c9def18ef98951840830bcdca60"><code>6c5bae8</code></a>
Merge branch '2.21' into 2.22</li>
<li><a
href="https://github.com/FasterXML/jackson-core/commit/fda71aa915d4637c2e8377725eeee9039b740500"><code>fda71aa</code></a>
Post-release dep version bump</li>
<li><a
href="https://github.com/FasterXML/jackson-core/commit/4ad623584c272a8b7de89ea6fd2bb40af1911b4a"><code>4ad6235</code></a>
[maven-release-plugin] prepare for next development iteration</li>
<li><a
href="https://github.com/FasterXML/jackson-core/commit/b5f62dd8cf4b6897bb18a43565495355e45b4e65"><code>b5f62dd</code></a>
[maven-release-plugin] prepare release jackson-core-2.21.5</li>
<li><a
href="https://github.com/FasterXML/jackson-core/commit/11f8b8c2730c48c8da188372af98b1b7a8219172"><code>11f8b8c</code></a>
Prep for 2.21.5 release</li>
<li><a
href="https://github.com/FasterXML/jackson-core/commit/492000443fcfc30bd97ae6a46d924120f39ce21c"><code>4920004</code></a>
Post-release version bump</li>
<li><a
href="https://github.com/FasterXML/jackson-core/commit/2bb55e8634c7fa1c197f995cdd4566ae7b7f4e0f"><code>2bb55e8</code></a>
[maven-release-plugin] prepare for next development iteration</li>
<li><a
href="https://github.com/FasterXML/jackson-core/commit/d763562443cc9d6e224a6265ca3710195c813cf5"><code>d763562</code></a>
[maven-release-plugin] prepare release jackson-core-2.22.0</li>
<li>Additional commits viewable in <a
href="https://github.com/FasterXML/jackson-core/compare/jackson-core-2.21.4...jackson-core-2.22.1">compare
view</a></li>
</ul>
</details>
<br />

Updates `com.fasterxml.jackson.core:jackson-databind` from 2.21.4 to
2.22.1
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/FasterXML/jackson/commits">compare
view</a></li>
</ul>
</details>
<br />

Updates `com.fasterxml.jackson.dataformat:jackson-dataformat-yaml` from
2.21.4 to 2.22.1
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/FasterXML/jackson-dataformats-text/commit/681f699ec98fec4202245384e46fb781ee92e747"><code>681f699</code></a>
[maven-release-plugin] prepare release
jackson-dataformats-text-2.22.1</li>
<li><a
href="https://github.com/FasterXML/jackson-dataformats-text/commit/8e2585234fecca6599e75b060f913495d12b418b"><code>8e25852</code></a>
Prep for 2.22.1 release</li>
<li><a
href="https://github.com/FasterXML/jackson-dataformats-text/commit/e1ed4d2cb6cebf7e657ee5b91732a20c60baf932"><code>e1ed4d2</code></a>
Merge branch '2.21' into 2.22</li>
<li><a
href="https://github.com/FasterXML/jackson-dataformats-text/commit/3f3257ce1b8f367ec024adff3145819f9f168c21"><code>3f3257c</code></a>
Post-release dep version bump</li>
<li><a
href="https://github.com/FasterXML/jackson-dataformats-text/commit/3348a1aac8d06a14d787ce69022241581f2bf409"><code>3348a1a</code></a>
[maven-release-plugin] prepare for next development iteration</li>
<li><a
href="https://github.com/FasterXML/jackson-dataformats-text/commit/687ad4f318cd633eb0000f888bbb936c328dddde"><code>687ad4f</code></a>
[maven-release-plugin] prepare release
jackson-dataformats-text-2.21.5</li>
<li><a
href="https://github.com/FasterXML/jackson-dataformats-text/commit/955c59fae23292e389a13715fde664f7d3484eb9"><code>955c59f</code></a>
Prep for 2.21.5 release</li>
<li><a
href="https://github.com/FasterXML/jackson-dataformats-text/commit/588933e4c87ea2ef7669d4659c770fbff04fc108"><code>588933e</code></a>
Post-release dep version bump</li>
<li><a
href="https://github.com/FasterXML/jackson-dataformats-text/commit/38f3683f4c7793dad8111cf823c17243541a74df"><code>38f3683</code></a>
[maven-release-plugin] prepare for next development iteration</li>
<li><a
href="https://github.com/FasterXML/jackson-dataformats-text/commit/23d487c5e2740c74668b67d9a0a98992248ce91f"><code>23d487c</code></a>
[maven-release-plugin] prepare release
jackson-dataformats-text-2.22.0</li>
<li>Additional commits viewable in <a
href="https://github.com/FasterXML/jackson-dataformats-text/compare/jackson-dataformats-text-2.21.4...jackson-dataformats-text-2.22.1">compare
view</a></li>
</ul>
</details>
<br />

Updates `com.fasterxml.jackson.datatype:jackson-datatype-jdk8` from
2.21.4 to 2.22.1

Updates `com.fasterxml.jackson.datatype:jackson-datatype-jsr310` from
2.21.4 to 2.22.1


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-01 17:07:02 +01:00
e76a22e520 fix(desktop): persist default-app banner dismiss and fix Linux detection (#7112)
# Description of Changes

Fixes the desktop default-PDF banner that kept returning every launch
after users dismissed it or already set Stirling as default
([#6743](https://github.com/Stirling-Tools/Stirling-PDF/issues/6743),
also tracked in
[#5772](https://github.com/Stirling-Tools/Stirling-PDF/issues/5772) /
[#6270](https://github.com/Stirling-Tools/Stirling-PDF/issues/6270)).

### What changed
- **Persist dismiss:** X closes the banner for the current session only;
**Don't remind me again** (muted secondary action) permanently opts out
via the existing `localStorage` helpers that were never wired.
- **Settings:** General → Default PDF editor includes a **Remind me to
set as default** toggle (on by default), shown only when Stirling is not
already the default, so users can undo a permanent dismiss.
- **Linux detection:** Treat `Stirling-PDF.desktop` / case-insensitive
`*stirling*.desktop` as default, and resolve the real desktop file when
setting the association (fixes “already default but banner still
shows”).
- **InfoBanner:** optional secondary button support for the muted “Don't
remind me again” action.

### macOS quirk (Gatekeeper false positive)
One-click **Set Default** on macOS still uses
`LSSetDefaultRoleHandlerForContentType` — Apple has no public
replacement for document UTIs, so this remains the only single-button
path.

After setting Stirling as default, if a user later switches away with
Finder **Open With → Always Open With** on a *quarantined* (typically
downloaded) PDF, macOS can show:

> Apple could not verify “…pdf” is free of malware…

That is a known Gatekeeper/`LSRiskCategoryHasRedirectedBinding`
behaviour ([Apple Developer Forums
thread](https://developer.apple.com/forums/thread/795994)), not malware
and not something we can suppress from the app. **Safe way to switch
away:** select a PDF → File → Get Info → Open With → choose the app →
**Change All** (avoid Open With → Always on downloaded PDFs).

Closes #6743

---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [x] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)
- New strings added in **en-US only** (`defaultApp.prompt.dontRemind`,
`settings.general.defaultPdfEditorRemind` / `RemindDescription`); other
locales handled separately.

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)
- Banner: session dismiss (X) + muted **Don't remind me again**;
settings toggle only when not already default.

### Testing (if applicable)

- [x] I have run `task check` to verify linters, typechecks, and tests
pass
- [x] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing)
for more details.

#### Test runs
- `task pre-commit:fix` (including toml-sort / locale hygiene)
- `task backend:test` — passed (incl. JaCoCo coverage targets)
- `task frontend:check` — lint, typecheck, format, tests
- `task frontend:test` — **166 files / 1353 tests passed**
- `task engine:check` — typecheck, lint, format; **335 pytest tests
passed**
- Re-ran `unusedTranslations` / `missingTranslations` after new en-US
keys — passed
- Manual: permanent dismiss persists across relaunch; settings remind
toggle restores banner; Linux desktop-file name mismatch addressed in
Rust

Co-authored-by: Wesley <wesley@awka.dev>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-08-01 16:30:53 +01:00
Ludy e565251991 ci(tauri): disable updater signing for unsigned builds (#7181)
# Description of Changes

This change updates the Tauri CI workflow to:

- Disable Tauri updater artifact generation for unsigned builds.
- Prevent unsigned builds from requiring updater signing secrets.
- Skip PR artifact comments for fork and Dependabot pull requests, where
the GitHub token is read-only.
- Preserve signed release builds and their updater artifacts unchanged.

The change prevents unsigned CI builds from failing because of
unavailable or invalid updater signing credentials. It also prevents
expected GitHub API permission errors from failing the workflow.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have run `task check` to verify linters, typechecks, and tests
pass
- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing)
for more details.
2026-08-01 14:28:47 +01:00
dependabot[bot] 47c68af068 build(deps): bump pyasn1 from 0.6.3 to 0.6.4 in /engine in the uv group across 1 directory (#7190)
Bumps the uv group with 1 update in the /engine directory:
[pyasn1](https://github.com/pyasn1/pyasn1).

Updates `pyasn1` from 0.6.3 to 0.6.4
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/pyasn1/pyasn1/releases">pyasn1's
releases</a>.</em></p>
<blockquote>
<h2>Release 0.6.4</h2>
<p>This is a security release.</p>
<ul>
<li>CVE-2026-59885 (GHSA-8ppf-4f7h-5ppj): Fixed quadratic time
complexity in the OBJECT IDENTIFIER and RELATIVE-OID decoders. A small
crafted substrate encoding many arcs could consume excessive CPU.</li>
<li>CVE-2026-59884 (GHSA-m4p7-r5rc-7g4j): Limited BER long-form tag IDs
to 20 octets (140 bits). Unbounded tag IDs allowed a crafted substrate
to consume excessive CPU and memory.</li>
<li>CVE-2026-59886 (GHSA-hm4w-wwcw-mr6r): Fixed excessive memory and CPU
consumption in <code>Real.__float__()</code> for values with large
base-10 exponents.</li>
<li>Pinned PyPI publish GitHub Action to an immutable commit.</li>
</ul>
<p>All changes are noted in the <a
href="https://github.com/pyasn1/pyasn1/blob/main/CHANGES.rst">CHANGELOG</a>.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/pyasn1/pyasn1/blob/main/CHANGES.rst">pyasn1's
changelog</a>.</em></p>
<blockquote>
<h2>Revision 0.6.4, released 08-07-2026</h2>
<ul>
<li>CVE-2026-59885 (GHSA-8ppf-4f7h-5ppj): Fixed quadratic time
complexity in the OBJECT IDENTIFIER and RELATIVE-OID decoders.
A small crafted substrate encoding many arcs could consume
excessive CPU. Arcs are now accumulated in linear time; decoded
values are unchanged (thanks for reporting, tynus2)</li>
<li>CVE-2026-59884 (GHSA-m4p7-r5rc-7g4j): Limited BER long-form tag
IDs to 20 octets (140 bits), matching the OID arc limit introduced
in 0.6.2. Unbounded tag IDs allowed a crafted substrate to consume
excessive CPU and memory; longer tag IDs are now rejected with
PyAsn1Error. Also fixed Tag and TagSet repr() failing on huge tag
(thanks for reporting, mikeappsec)
IDs due to the integer-to-string conversion limit (Python 3.11+)</li>
<li>CVE-2026-59886 (GHSA-hm4w-wwcw-mr6r): Fixed excessive memory and
CPU consumption in Real.<strong>float</strong>() for values with large
base-10
exponents. Conversion no longer materializes huge intermediate
integers; values too large to represent as a Python float raise
OverflowError promptly, and prettyPrint() renders them as
'<!-- raw HTML omitted -->' as before. Also fixed base-10 mantissa
normalization
to use exact integer arithmetic; mantissas larger than 2**53
could previously lose precision through float division
(thanks for reporting, gvozdila)</li>
<li>Pinned PyPI publish GitHub Action to an immutable commit
[pr <a
href="https://redirect.github.com/pyasn1/pyasn1/issues/113">#113</a>](<a
href="https://redirect.github.com/pyasn1/pyasn1/pull/113">pyasn1/pyasn1#113</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/pyasn1/pyasn1/commit/72e4803405816c371ed3b2cb4be181c47f038406"><code>72e4803</code></a>
Prepare release 0.6.4</li>
<li><a
href="https://github.com/pyasn1/pyasn1/commit/0c19eeb853731db1c717ff125ea001a1e558332d"><code>0c19eeb</code></a>
Pin PyPI publish action to immutable commit (<a
href="https://redirect.github.com/pyasn1/pyasn1/issues/113">#113</a>)</li>
<li><a
href="https://github.com/pyasn1/pyasn1/commit/45bdb19eb7df4b3780fe9c912c63e99bffc39dd9"><code>45bdb19</code></a>
Merge commit from fork</li>
<li><a
href="https://github.com/pyasn1/pyasn1/commit/628e36ecbb5277a3f01572ce418ef54271b165a5"><code>628e36e</code></a>
Merge commit from fork</li>
<li><a
href="https://github.com/pyasn1/pyasn1/commit/e60c691cb91addb8fcefa2f537e85ede6fb1e886"><code>e60c691</code></a>
Merge commit from fork</li>
<li>See full diff in <a
href="https://github.com/pyasn1/pyasn1/compare/v0.6.3...v0.6.4">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pyasn1&package-manager=uv&previous-version=0.6.3&new-version=0.6.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/Stirling-Tools/Stirling-PDF/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-01 10:55:16 +00:00
dependabot[bot] 5412dd4d4f build(deps): bump dorny/paths-filter from 4.0.1 to 4.0.2 (#6987)
Bumps [dorny/paths-filter](https://github.com/dorny/paths-filter) from
4.0.1 to 4.0.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/dorny/paths-filter/releases">dorny/paths-filter's
releases</a>.</em></p>
<blockquote>
<h2>v4.0.2</h2>
<h2>What's Changed</h2>
<ul>
<li>fix warning message by <a
href="https://github.com/cgundy"><code>@​cgundy</code></a> in <a
href="https://redirect.github.com/dorny/paths-filter/pull/282">dorny/paths-filter#282</a></li>
<li>chore: fix GitHub spelling in logs by <a
href="https://github.com/squat"><code>@​squat</code></a> in <a
href="https://redirect.github.com/dorny/paths-filter/pull/278">dorny/paths-filter#278</a></li>
<li>fix: use rev-parse instead of branch --show-current for older git
compat by <a
href="https://github.com/saschabratton"><code>@​saschabratton</code></a>
in <a
href="https://redirect.github.com/dorny/paths-filter/pull/303">dorny/paths-filter#303</a></li>
<li>fix: work around git dubious ownership errors in container jobs by
<a
href="https://github.com/saschabratton"><code>@​saschabratton</code></a>
in <a
href="https://redirect.github.com/dorny/paths-filter/pull/317">dorny/paths-filter#317</a></li>
<li>docs: update changelog for v4.0.2 by <a
href="https://github.com/saschabratton"><code>@​saschabratton</code></a>
in <a
href="https://redirect.github.com/dorny/paths-filter/pull/318">dorny/paths-filter#318</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/cgundy"><code>@​cgundy</code></a> made
their first contribution in <a
href="https://redirect.github.com/dorny/paths-filter/pull/282">dorny/paths-filter#282</a></li>
<li><a href="https://github.com/squat"><code>@​squat</code></a> made
their first contribution in <a
href="https://redirect.github.com/dorny/paths-filter/pull/278">dorny/paths-filter#278</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/dorny/paths-filter/compare/v4.0.1...v4.0.2">https://github.com/dorny/paths-filter/compare/v4.0.1...v4.0.2</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/dorny/paths-filter/blob/master/CHANGELOG.md">dorny/paths-filter's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>v4.0.2</h2>
<ul>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/317">Work
around git dubious ownership errors in container jobs</a></li>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/303">Use
rev-parse instead of branch --show-current for older git compat</a></li>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/282">Fix
warning message</a></li>
</ul>
<h2>v4.0.1</h2>
<ul>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/255">Support
merge queue</a></li>
</ul>
<h2>v4.0.0</h2>
<ul>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/294">Update
action runtime to node24</a></li>
</ul>
<h2>v3.0.3</h2>
<ul>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/279">Add
missing predicate-quantifier</a></li>
</ul>
<h2>v3.0.2</h2>
<ul>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/224">Add
config parameter for predicate quantifier</a></li>
</ul>
<h2>v3.0.1</h2>
<ul>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/133">Compare
base and ref when token is empty</a></li>
</ul>
<h2>v3.0.0</h2>
<ul>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/210">Update to
Node.js 20</a></li>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/215">Update
all dependencies</a></li>
</ul>
<h2>v2.11.1</h2>
<ul>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/167">Update
@​actions/core to v1.10.0 - Fixes warning about deprecated
set-output</a></li>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/168">Document
need for pull-requests: read permission</a></li>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/164">Updating
to actions/checkout@v3</a></li>
</ul>
<h2>v2.11.0</h2>
<ul>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/157">Set
list-files input parameter as not required</a></li>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/161">Update
Node.js</a></li>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/162">Fix
incorrect handling of Unicode characters in exec()</a></li>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/163">Use
Octokit pagination</a></li>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/160">Updates
real world links</a></li>
</ul>
<h2>v2.10.2</h2>
<ul>
<li><a href="https://redirect.github.com/dorny/paths-filter/pull/91">Fix
getLocalRef() returns wrong ref</a></li>
</ul>
<h2>v2.10.1</h2>
<ul>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/85">Improve
robustness of change detection</a></li>
</ul>
<h2>v2.10.0</h2>
<ul>
<li><a href="https://redirect.github.com/dorny/paths-filter/pull/82">Add
ref input parameter</a></li>
<li><a href="https://redirect.github.com/dorny/paths-filter/pull/83">Fix
change detection in PR when pullRequest.changed_files is
incorrect</a></li>
</ul>
<h2>v2.9.3</h2>
<ul>
<li><a href="https://redirect.github.com/dorny/paths-filter/pull/78">Fix
change detection when base is a tag</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/dorny/paths-filter/commit/7b450fff21473bca461d4b92ce414b9d0420d706"><code>7b450ff</code></a>
docs: update changelog for v4.0.2 (<a
href="https://redirect.github.com/dorny/paths-filter/issues/318">#318</a>)</li>
<li><a
href="https://github.com/dorny/paths-filter/commit/928037783a71f24983ea250c6e55290c1b2de54f"><code>9280377</code></a>
fix: work around git dubious ownership errors in container jobs (<a
href="https://redirect.github.com/dorny/paths-filter/issues/317">#317</a>)</li>
<li><a
href="https://github.com/dorny/paths-filter/commit/f3ceefdc7ef57bc2d8560787d4b6c33e44044cec"><code>f3ceefd</code></a>
fix: use rev-parse instead of branch --show-current for older git compat
(<a
href="https://redirect.github.com/dorny/paths-filter/issues/303">#303</a>)</li>
<li><a
href="https://github.com/dorny/paths-filter/commit/61f87a10cd2c304679af17bb73ef192addf33c1c"><code>61f87a1</code></a>
chore: fix GitHub spelling in logs (<a
href="https://redirect.github.com/dorny/paths-filter/issues/278">#278</a>)</li>
<li><a
href="https://github.com/dorny/paths-filter/commit/b82ff81ffbe6fb4b636bb5b47e37fd8d12b32632"><code>b82ff81</code></a>
fix warning message (<a
href="https://redirect.github.com/dorny/paths-filter/issues/282">#282</a>)</li>
<li>See full diff in <a
href="https://github.com/dorny/paths-filter/compare/fbd0ab8f3e69293af611ebaee6363fc25e6d187d...7b450fff21473bca461d4b92ce414b9d0420d706">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=dorny/paths-filter&package-manager=github_actions&previous-version=4.0.1&new-version=4.0.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-01 10:55:10 +00:00
dependabot[bot] d8c6af2e7a build(deps): bump eclipse-temurin from b27ca47 to 2f1da10 in /docker/base (#6555)
Bumps eclipse-temurin from `b27ca47` to `2f1da10`.


> **Note**
> Automatic rebases have been disabled on this pull request as it has
been open for over 30 days.

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-01 10:55:05 +00:00
EthanHealy01 9d473fb435 Resolve duplicate CSS selectors reported by Stylelint (#7212)
Fixes #7189

Consolidates all 29 `no-duplicate-selectors` violations across 18
stylesheets by merging later duplicate rule blocks into the first
occurrence. Where duplicates had conflicting values, the cascade-winning
(later) value was kept, so computed styles are unchanged.

Also:
- Adds `frontend/stylelint.config.mjs` with only
`no-duplicate-selectors` enabled (Prettier and the theme linter own
everything else).
- Adds a `frontend:lint:css` task, wired into `task frontend:lint` as a
blocking check so regressions can't creep back in.
- Lints all first-party CSS (`editor/**/*.css`, so `public/css` and any
future non-`src` stylesheets are covered too), excluding only the
vendored `cookieconsent.css` and build output via `ignoreFiles`. This
surfaced and fixed 5 additional duplicate selectors in
`cookieconsentCustomisation.css` that weren't in the original issue
report.

Note: `portal/views/Sources.css` goes beyond dedup — the whole
`.portal-sources__connections*` block is deleted as dead code
(unreferenced since the S3 connections redesign in #6965; only
`-actions` in it was an actual duplicate).

🤖 Generated with [Claude Code](https://claude.ai/code)
2026-07-31 16:46:31 +00:00
James Brunton 041b6a4b5f Move parameter validation to config-accessible function (#7224)
# Description of Changes
Move the logic for validating params into global exported functions and
attach them to the operation config so that we can access them elsewhere
so things like Pipelines know whether the tool has been configured and
provide a warning if not.

<img width="1264" height="503" alt="image"
src="https://github.com/user-attachments/assets/f320579c-6c08-4c66-a999-015d46df1b33"
/>
2026-07-31 15:59:16 +00:00
ConnorYoh 8990f55e50 feat(storage): encryption at rest for stored files (per-team envelope encryption) (#7155)
# Description of Changes

PR1 of the encrypt-at-rest initiative: user files stored by Stirling (My
Files, workflow files) are now AES-256 encrypted at rest across all
three storage backends, with keys that never leave the deployment.

**What was changed**

- New `EncryptingStorageProvider` decorator wraps whichever
`StorageProvider` backend is configured (local / database / S3). It
encrypts on `store` (Tink AES-256-GCM streaming AEAD, 1 MiB segments)
and transparently decrypts on `load`; legacy plaintext blobs are
detected by magic sniff and pass through untouched, so mixed state is
safe and no migration is required to enable.
- Envelope-encryption key hierarchy: each blob gets a random per-file
DEK, wrapped by a per-team KEK stored (master-key-wrapped) in a new
`file_encryption_keys` registry table; the master key resolves like the
existing credential key — `stirling.security.fileEncryptionKey`
property, `STIRLING_FILE_ENCRYPTION_KEY` env var, or an auto-generated
owner-only `file-encryption.key` in the config dir (cluster mode
requires an explicit shared key, fail-fast).
- Self-describing blob format (`SPDFEAR1` header) carrying the key id,
plaintext length, and the wrapped DEK; the header prefix is bound as GCM
associated data to both the DEK wrap and the payload, so headers cannot
be transplanted between blobs.
- Enabled via `storage.encryption.enabled=true`, gated on a
Pro/Enterprise licence — **write side only**: decryption activates
whenever key rows exist, so switching the flag off or a lapsed licence
can never make previously encrypted files unreadable.
- Key status lifecycle (`ACTIVE`/`RETIRED`/`DISABLED`): `DISABLED` is a
reversible per-team kill switch that fails closed on read; no API path
deletes key material. A revoked download surfaces as **403 Forbidden**
("access revoked"), not a 500, since it is a deliberate policy state
rather than a fault.
- Startup self-check: a master key that cannot unwrap existing key rows
refuses to boot rather than silently starting a second key hierarchy.
- S3 presigned download URLs are suppressed for decorated storage (they
would serve ciphertext); the controller already falls back to
app-streamed downloads.
- `StoredFile`/`StoredObject` gain a nullable `encryption_key_id`
(ddl-auto, no migration); persisted sizes remain plaintext sizes so
quotas and UI are unchanged.

**Why**

Enterprise security questionnaires (and HIPAA/GDPR/CMMC buyers) require
encryption at rest with documented key management; files were previously
plaintext in every backend. Design doc and vendor/standards research
(Purview, Box KeySafe, Google CSE, ISO 32000-2) informed the approach.

## Manually tested end-to-end

Beyond the automated suite, the full flow was exercised against a
running backend (local provider, `storage.encryption.enabled=true`,
login enabled) via the storage API:

1. **Startup** — master key auto-generated with the "back this up"
warning; logs `master key initialised (AES-256-GCM, fingerprint …)` and
`Storage encryption at rest active (writes encrypted)`.
2. **Encrypted at rest** — uploaded a PDF containing a known marker
string; the blob on disk (371 B vs 219 B plaintext) began with the
`SPDFEAR1` header + key id + ciphertext, contained **no `%PDF` signature
and no marker** — not openable as a PDF straight off disk.
3. **Transparent access** — downloading the file through the API
returned it **byte-identical** to the original, marker intact; stored
`sizeBytes` stayed the plaintext size.
4. **Kill switch + reversibility** — set the team key's status directly
in the DB and restarted:
- `DISABLED` → download **failed closed** (`403`, "access to this
content is revoked"), zero plaintext served.
- `ACTIVE` again → file **fully recovered, byte-identical**. Disabling
is a reversible switch on a preserved key row, not destruction.

(The 403 mapping in step 4 was added in this PR after the manual run
first surfaced it as a generic 500.)

## Coming in later PRs

- **PR2 — ops & lifecycle:** audit events for
encrypt/decrypt/key-lifecycle; admin endpoints for the kill switch
(disable/enable) and key status; a background "encrypt existing files"
migration job for turning the feature on over pre-existing plaintext;
master-key rotation (re-wrap KEK rows). Also plans a
key-backup/fingerprint verification command.
- **PR3 — admin UI:** settings section (status, per-team key list with
disable/enable), encrypted-file badge in My Files, i18n.
- **Later:** per-**source** encryption for the Processor pipeline (the
`SOURCE` key scope is already reserved in the schema); pluggable
external KMS / BYOK master-key backends (Vault, AWS/Azure/GCP KMS);
optional FIPS-validated crypto module build for CMMC; and encrypted
egress (PDF-native AES-256) for files leaving the platform.

**Reviewer notes**

- New dependency: `com.google.crypto.tink:tink:1.23.0` (Apache-2.0, pure
Java — bundled in the boot jar, no Docker changes). Pulls protobuf-java
4.33.6, which clears the Aikido-flagged CVE-2024-7254. `./gradlew
checkLicense --no-parallel` passes.
- The `file-encryption.key` file is generated in the config dir on first
use and must be backed up; losing it makes encrypted files unrecoverable
(loud log warning + fingerprint exposed for backup verification).
- Tests cover round-trips on re-openable and one-shot (S3-style)
backends, multi-segment files, legacy passthrough, decrypt-only mode,
disabled-key fail-closed (now asserting the 403 mapping), header/payload
tamper rejection, key-creation races, and presigned-URL suppression.

---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [x] I have run `task check` to verify linters, typechecks, and tests
pass
- [x] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing)
for more details.
2026-07-31 14:57:19 +00:00
ConnorYoh d5e3c3e6e1 Remove dead frontend exports and their orphaned translation keys (#7226)
## Why

Spotted while working on #7222. `PIPELINE_OPERATIONS` reads like the
list that decides which tools the pipeline composer offers — its doc
comment literally said *"the operation catalogue the composer builds a
pipeline from"* and *"adding an operation needs no translation work"*.
Nothing has imported it since #6905 replaced it with
`getExecutableTools` over the tool registry.

That makes it worse than dead weight: I added an entry to it to expose
Auto Rotate, and the entry did nothing. The next person will do the
same.

So I swept the editor frontend for the same shape — exported values with
no reference anywhere, including inside their own file — and removed the
confirmed ones.

## What went

**6 wholly dead files**
- `core/components/tooltips/useCertSignTooltips.ts`
- `core/components/tooltips/useCertificateChoiceTips.ts`
- `core/components/tooltips/useSessionManagementTips.ts`
- `core/components/tooltips/useWetSignatureTips.ts`
- `desktop/config/planFeatures.ts`
- `core/hooks/tools/shared/useOperationResults.ts`

**Dead consts and helpers** across core, portal and desktop:
`PIPELINE_OPERATIONS` + `PipelineOperationDef`, `FILE_ORIGINS`,
`DATE_GROUP_ORDER`, `toRgba`, `isSplitMethod`, `useSignatureMode`,
`getCategoryLabel`, `useCommonTranslations`, `useSignatureDetection`,
two `Z_INDEX_*` tokens, `getFontStatusIcon`, `buildUpdatedDocument`,
`INFO_PDF_FILENAME`, `useCustomMetadataTips`, `PORTAL_ACCESS_TONE`,
`MEMBER_STATUS_TONE`, `ASSIGNABLE_ROLES`, `TIER_INFO`, `VIEW_LABELS`,
`lookupVertical`, `lookupEndpoint`, `lookupAgent`, `lookupSource`,
`lookupDestination`, `docsSource`.

`fileProcessingService.ts` keeps only `ProcessedFileMetadata`, which
four files still import. Removing the dead service instance orphaned the
`FileProcessingService` class and its result type, so those went with
it.

**103 en-US translation keys** that only the deleted tooltip hooks and
helpers referenced — the unused-translation audit flags these as soon as
their callers go, so the cleanup isn't complete without them. Only
`en-US` is touched, since that is what the audit gates.

## Kept deliberately

Three things look unused in `core` but are not, and a naive sweep would
have deleted them:

- `Z_ANALYTICS_MODAL` and `DEV_TESTING_ENABLED` — the `saas` and
`proprietary` layers shadow those modules and reference them
- `CLOUD_LAYER_PROBE` — exists to prove layer resolution

## Testing

Deletions only, so the type checker and the suites are the proof:

- `typecheck:all` + `typecheck:portal` — all eight build variants clean
- full `eslint` (zero warnings) and `theme-lint`
- `prettier --check` across the frontend
- full editor suite: **1651 tests, 186 files, all passing**

Three incomplete removals and two orphaned imports were caught by
exactly those gates and fixed before pushing.

## Note

The same 103 keys still exist in the other ~40 locale files. The audit
only gates `en-US`, and translations are usually synced separately, so I
left them — worth a follow-up if you want them pruned too.
2026-07-31 14:55:46 +00:00
ConnorYoh 6094040e0a Make Auto Rotate's settings editable in the pipeline composer (#7222)
Follow-up to #7152.

## Why

Auto Rotate was already selectable as a pipeline step —
`getExecutableTools` lists any registry tool with an `operationConfig`
and an endpoint resolvable from defaults, which it has. But once added,
the settings pane showed *"Displaying these tool params for editing is
not supported yet"* and the step card read *"Runs with default
settings"*.

Both come from the same cause. `classifyToolStepSupport` keys off the
operation config's mappers:

```js
const hasMappers = Boolean(config?.toApiParams && config?.fromApiParams);
if (!hasMappers) return "unsupported";
return entry.automationSettings ? "editable" : "noSettings";
```

Auto Rotate had `automationSettings` but no mappers, because its custom
processor builds its own FormData and never needed them — so it
classified as `unsupported`.

The more consequential half is `serializeToolStep`, which emits
`parameters: {}` for a mapper-less tool. A composed pipeline therefore
ran Auto Rotate on server defaults with no way to change that. "Runs
with default settings" was literal.

## What

Declares `toApiParams` / `fromApiParams` on `autoRotateOperationConfig`,
mapping `detectionMode`, `confidenceThreshold` and `inferUndetected`.

That flips the step to `editable`, so the composer reuses the existing
`AutoRotateAutomationSettings` panel, and the chosen settings now reach
the backend.

**No backend change.** Policy steps already serialise `parameters` as
form fields and the endpoint already accepts these three — verified
against a running server while working on #7152.

## Testing

- New round-trip test in `toolAutomation.test.ts`: the step serialises
to `{detectionMode, confidenceThreshold, inferUndetected}` and
deserialises back with `support: "editable"`, using the real operation
config.
- `core/hooks/tools/shared` + `portal/components/pipelines` suites: 63
tests pass.
- Typecheck across build variants, ESLint, Prettier.

The composer UI itself was not clicked: the portal is not served by the
editor dev server locally, so the verification is the unit test plus the
render path (`PipelineStepSettings` renders `entry.automationSettings`
when support is `editable`, and the registry entry supplies it). Worth a
click-through on the preview deploy.

## Note for maintainers

While tracing this I found `PIPELINE_OPERATIONS` in
`portal/components/pipelines/pipelineOperations.ts` is exported but
never imported — the composer builds its list from the tool registry
instead. `humanizeOperation` in the same file *is* still used as a label
fallback. Left alone here as it is out of scope, but it looks like dead
code worth deleting separately.

Generated with [Claude Code](https://claude.com/claude-code)
2026-07-31 13:52:44 +00:00
James Brunton 6933d11bce Type-check Storybook files (#7223)
# Description of Changes
Storybook files aren't currently being type-checked, but they should be.
A bunch of them had a dodgy import in them (which didn't affect anything
because it was just an `import type` but still worth fixing).
2026-07-31 13:20:07 +00:00
ConnorYoh 46865cd154 Add Auto Rotate tool that detects and fixes page orientation (#7152)
## What

New **Auto Rotate** tool: give it any PDF and it detects each page's
correct orientation and sets `/Rotate` so every page displays upright.
Lossless — only the page rotation metadata changes, content is never
re-rendered.

New endpoint: `POST /api/v1/misc/auto-rotate-pdf`, plus an editor tool
registered next to Rotate.

## How it works

Two-tier detection, per page, both expressed as an additive clockwise
`/Rotate` correction:

1. **Embedded-text fast path** (`AutoRotateDetection`): dominant glyph
direction via `PDFTextStripper`/`TextPosition.getDir()`. Trusted only
with >= 30 glyphs at >= 95% agreement. Near-instant for born-digital
PDFs and needs no external tools. Correction = `(glyphDir -
pageRotation) mod 360` — the sign conventions are pinned by
parameterized fixture tests covering all text-angle x `/Rotate`
combinations.
2. **Tesseract OSD fallback**: pages the text path can't decide are
rendered at 300 DPI grayscale and run through `tesseract --psm 0`.
Corrections apply only above a confidence threshold (default 14.0,
matching OCRmyPDF's `--rotate-pages-threshold`). Rendering honours the
existing `/Rotate`, so the verdict is always additive.

**Conservative by default**: blank pages, mixed-direction pages, and
low-confidence verdicts are skipped, never guessed — the failure mode to
avoid is making a correct page wrong.

### API surface

- `detectionMode`: `auto` (default) | `text` | `osd` — forcing one
method is useful for testing
- `confidenceThreshold`: minimum OSD confidence to apply a correction
- `dryRun=true`: returns a JSON per-page report instead of the PDF
- `pageRotations={"1":90,...}`: applies precomputed corrections without
detection

The frontend uses analyze-then-apply (dryRun, then pageRotations) so
detection runs exactly once per file, and the analysis report can be
shown in the UI.

### UI

The tool's results panel shows a **detection report** for
debugging/tuning: per page — method badge (Text / OCR / Skipped),
confidence score (glyph-dominance % for text, raw OSD score for OCR),
applied rotation, and a skip reason (too little text, mixed directions,
below threshold, OCR not installed...). Settings expose detection mode
and the OSD threshold.

### Dependency handling

Registered in `PageOps` only — deliberately **not** gated on the
`tesseract` group, because the text path works without Tesseract. The
controller checks `isGroupEnabled("tesseract")` at runtime; when it's
missing, scanned pages are skipped with a visible `tesseractUnavailable`
note instead of the whole tool disappearing.

## Testing

- 14 detection unit tests: all text-angle x `/Rotate` fixture
combinations (pins the direction conventions), dominance/glyph-count
guards, OSD output parsing
- 6 controller tests: dryRun report, correction application, explicit
pageRotations, tesseract-unavailable reporting, input validation
- Frontend: typecheck (core/desktop/proprietary), ESLint, Prettier, all
i18n audit tests
- **Live, text path**: fixture with pages at `/Rotate` 0/90/180/270 ->
all pages return upright; report UI verified in the browser
- **Live, OSD path**: image-only "scan" fixture (no text layer) with
pages upright/180/90 -> all detected by OSD at conf ~15-17 and
corrected; closed-loop re-analysis of the output reports 0 pages to
rotate with *higher* confidence than the input

Out of scope: skew correction (that's the OCR tool's `--deskew`); this
fixes 90-degree-multiple orientation only.
2026-07-31 11:27:12 +00:00
James Brunton 3bee6d212e Change pipelines to have 1 input and 1 output (#7121)
# Description of Changes

Change pipelines so that sources and triggers are grouped into a list of
inputs, so you can have a different trigger for each source in the list.
This is necessary because triggers are not universally supported by all
source types. If you wanted to have a pipeline pull from both a folder
and an S3 bucket, the current system allows you to choose "Folder Watch"
as the trigger, which will either do nothing or crash when it's paired
with the S3 bucket.

I've got reservations about actually allowing different triggers for
every source because it allows for user workflows that I don't believe
exist, like "I want this folder to be polled every minute and this other
one to be polled every hour, but they should run the same tools and
should output to the same place". Because of this (with agreement from
Connor, Anthony and Matt) I've changed this PR to artificially limit
pipelines to having 1 input & output at this stage. The backend is still
shaped to support multiple inputs & outputs so it should be trivial to
re-add support for them in the future if we decide we want to, but the
UI can be much simpler and easier to understand with just 1 input and
output.

<img width="1262" height="521" alt="image"
src="https://github.com/user-attachments/assets/809e6803-9f99-436d-9aeb-52dddf0906ff"
/>
2026-07-31 08:53:01 +00:00
Frooodle ca3b87e7fd fix variable field a11y 2026-07-30 12:32:39 +01:00
Frooodle 7435b4f7ed Merge branch 'main' into integration-tasks-and-variables 2026-07-30 12:32:39 +01:00
Frooodle a4f1696aba Merge branch 'main' into integration-tasks-and-variables 2026-07-29 16:25:49 +01:00
Frooodle 9eb8e2b7fd bound recursion depth 2026-07-29 09:38:48 +01:00
Frooodle 08d670cfb3 Merge branch 'main' into integration-tasks-and-variables 2026-07-29 09:22:06 +01:00
Anthony Stirling a9c50f2501 Fix review findings for cross-step variables and integration tasks 2026-07-28 12:40:53 +01:00
Anthony Stirling 0b953f26b3 Add variable tokens, autocomplete and reference to integration steps 2026-07-24 15:38:56 +01:00
Anthony Stirling 6b4e20b705 Add multi-task integration operations and per-integration task lists 2026-07-24 15:38:56 +01:00
Anthony Stirling 4e73d62530 Guard source modal edit seed against stale fetches 2026-07-24 15:38:56 +01:00
Anthony Stirling 8f50e373c4 Support cross-step variables and size caps in integration steps 2026-07-24 15:38:56 +01:00
335 changed files with 14376 additions and 2875 deletions
+6 -6
View File
@@ -35,7 +35,7 @@ jobs:
pr_ref: ${{ steps.resolve.outputs.ref }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
@@ -115,7 +115,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
@@ -184,7 +184,7 @@ jobs:
fetch-depth: 0 # Fetch full history for commit hash detection
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Get version number
id: versionNumber
@@ -193,7 +193,7 @@ jobs:
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
- name: Login to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}
@@ -320,7 +320,7 @@ jobs:
# under the exact same access rules as the app preview.
- name: Detect Storybook changes
id: sb-changes
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
with:
list-files: json
filters: |
@@ -456,7 +456,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
@@ -70,7 +70,7 @@ jobs:
enable_prototypes: ${{ steps.check-prototypes-flag.outputs.enable_prototypes }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
@@ -183,7 +183,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
@@ -233,10 +233,10 @@ jobs:
STIRLING_PDF_DESKTOP_UI: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Login to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}
@@ -474,7 +474,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
is_fork: ${{ steps.decide.outputs.is_fork }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
pull-requests: write
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
+2 -2
View File
@@ -19,7 +19,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
@@ -87,7 +87,7 @@ jobs:
- name: AI PR Title Analysis
if: steps.actor.outputs.is_repo_dev == 'true'
id: ai-title-analysis
uses: actions/ai-inference@17ff458cb182449bbb2e43701fcd98f6af8f6570 # v2.1.0
uses: actions/ai-inference@a7805884c80886efc241e94a5351df715968a0ad # v2.1.1
with:
model: openai/gpt-4o
system-prompt-file: ".github/config/system-prompt.txt"
+2 -2
View File
@@ -26,7 +26,7 @@ jobs:
jar_sha256: ${{ steps.hashes.outputs.jar_sha256 }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
@@ -70,7 +70,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
flavor: [core, proprietary, saas]
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
- name: Checkout repository
+68 -1
View File
@@ -50,7 +50,7 @@ jobs:
SYSTEM_ENABLEANALYTICS: "false"
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
- name: Checkout repository
@@ -295,3 +295,70 @@ jobs:
name: playwright-report-enterprise-${{ github.run_id }}
path: frontend/playwright-report/
retention-days: 7
# Multi-node regression: builds + seeds the clustered stack (testing/compose/docker-compose-multinode.yml)
# and runs behave features/multinode. Licence-gated, so it runs after the Playwright job (not in parallel).
multinode-e2e:
needs: [pick, playwright-e2e-enterprise]
# Nightly cron + manual dispatch only (heavy build), fork-gated for the licence secret.
if: >-
always() && needs.pick.outputs.is_fork != 'true'
&& (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
runs-on: ${{ needs.pick.outputs.is_fork == 'true' && 'ubuntu-latest' || format('depot-ubuntu-24.04-{0}', inputs.depot_cores || '8') }}
timeout-minutes: 60
env:
PREMIUM_KEY: ${{ secrets.PREMIUM_KEY_ENTERPRISE }}
PREMIUM_ENABLED: "true"
SYSTEM_ENABLEANALYTICS: "false"
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
MN_COMPOSE: docker-compose-multinode.yml
steps:
- name: Harden Runner
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
cache: "pip"
cache-dependency-path: ./testing/cucumber/requirements.txt
- name: Install behave test deps
run: |
pip install --require-hashes --only-binary=:all: -r ./testing/cucumber/requirements.txt
- name: Build the multi-node image
working-directory: testing/compose
run: docker compose -f "$MN_COMPOSE" build
- name: Bring up the cluster and wait for both nodes healthy
working-directory: testing/compose
run: |
docker compose -f "$MN_COMPOSE" up -d
for i in $(seq 1 90); do
h1=$(docker inspect -f '{{.State.Health.Status}}' multinode-stirling-1 2>/dev/null || echo starting)
h2=$(docker inspect -f '{{.State.Health.Status}}' multinode-stirling-2 2>/dev/null || echo starting)
if [ "$h1" = healthy ] && [ "$h2" = healthy ]; then echo "both nodes healthy"; exit 0; fi
sleep 5
done
echo "::error::nodes did not become healthy"
docker compose -f "$MN_COMPOSE" logs --tail=200 stirling-1 stirling-2
exit 1
- name: Seed the cluster (teams, users, S3 connection, policy)
working-directory: testing/compose
run: docker compose -f "$MN_COMPOSE" --profile seed run --rm seed
- name: Run multi-node regression (implemented guarantees)
working-directory: testing/cucumber
# -e overrides behave.ini's exclusion of features/multinode; ~@known_gap skips any tracked-gap scenarios.
run: python -m behave features/multinode -e "features/enterprise" --tags="~@known_gap ~@destructive" --no-capture -f plain
- name: Run multi-node failover (destructive)
working-directory: testing/cucumber
run: python -m behave features/multinode -e "features/enterprise" --tags="@destructive ~@known_gap" --no-capture -f plain
- name: Dump node logs on failure
if: failure()
working-directory: testing/compose
run: docker compose -f "$MN_COMPOSE" logs --tail=400 stirling-1 stirling-2
- name: Tear down
if: always()
working-directory: testing/compose
run: docker compose -f "$MN_COMPOSE" --profile seed down -v --remove-orphans
+3 -3
View File
@@ -48,14 +48,14 @@ jobs:
proprietary: ${{ steps.changes.outputs.proprietary }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Check for file changes
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
id: changes
with:
filters: .github/config/.files.yaml
@@ -267,7 +267,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
pull-requests: write
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
pull-requests: write # Allow writing to pull requests
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
timeout-minutes: 15
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
- name: Checkout repository
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
timeout-minutes: 30
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
+3 -3
View File
@@ -18,7 +18,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
@@ -26,7 +26,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Get commit hashes for frontend and backend
id: commit-hashes
@@ -85,7 +85,7 @@ jobs:
fi
- name: Login to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}
+2 -2
View File
@@ -25,7 +25,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
@@ -61,7 +61,7 @@ jobs:
# runtime token isn't exposed) since the docker driver can't use it.
- name: Set up Docker Buildx
if: inputs.docker-base-changed != 'true'
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
# Expose ACTIONS_RUNTIME_TOKEN / ACTIONS_RESULTS_URL for docker buildx type=gha cache backend.
- name: Expose GitHub runtime for Buildx cache
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
timeout-minutes: 30
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
- name: Checkout repository
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
- name: Checkout repository
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
timeout-minutes: 25
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
- name: Checkout repository
@@ -28,7 +28,7 @@ jobs:
licenses-backend: ${{ steps.changes.outputs.licenses-backend }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
@@ -36,7 +36,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Check for file changes
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
id: changes
with:
filters: .github/config/.files.yaml
@@ -52,7 +52,7 @@ jobs:
repository-projects: write # Required for enabling automerge
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
@@ -326,7 +326,7 @@ jobs:
repository-projects: write # Required for enabling automerge
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
- name: Checkout repository
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
issues: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
+4 -4
View File
@@ -44,7 +44,7 @@ jobs:
version: ${{ steps.versionNumber.outputs.versionNumber }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
@@ -127,7 +127,7 @@ jobs:
file_suffix: "-server"
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
@@ -191,7 +191,7 @@ jobs:
RELEASE_GPG_PRIVATE_KEY: ${{ secrets.RELEASE_GPG_PRIVATE_KEY }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
allowed-endpoints: >
@@ -637,7 +637,7 @@ jobs:
contents: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
+2 -2
View File
@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
@@ -64,7 +64,7 @@ jobs:
timeout-minutes: 60
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
+2 -2
View File
@@ -28,7 +28,7 @@ jobs:
jar_sha256: ${{ steps.hashes.outputs.jar_sha256 }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
@@ -79,7 +79,7 @@ jobs:
contents: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
pull-requests: read
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
+4 -4
View File
@@ -43,20 +43,20 @@ jobs:
echo "version=${VERSION}" >> $GITHUB_OUTPUT
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Login to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -64,7 +64,7 @@ jobs:
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Set up QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
+4 -4
View File
@@ -53,7 +53,7 @@ jobs:
RUN_UNOSERVER: ${{ github.event_name != 'workflow_dispatch' || inputs.build_unoserver }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
@@ -82,7 +82,7 @@ jobs:
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Install Task
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
@@ -107,13 +107,13 @@ jobs:
cosign-release: "v2.4.1"
- name: Login to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
with:
registry: ghcr.io
username: ${{ github.actor }}
+3 -3
View File
@@ -18,7 +18,7 @@ jobs:
packages: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
@@ -26,13 +26,13 @@ jobs:
uses: imjasonh/setup-crane@31b88afe9de28ae0ffa220711af4b60be9435f6e # v0.4
- name: Login to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
with:
registry: ghcr.io
username: ${{ github.actor }}
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
+15 -6
View File
@@ -62,7 +62,7 @@ jobs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
@@ -110,7 +110,7 @@ jobs:
RELEASE_GPG_PRIVATE_KEY: ${{ secrets.RELEASE_GPG_PRIVATE_KEY }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
@@ -420,7 +420,9 @@ jobs:
# Linux: build deb+rpm only here (deb-only on minimal smoke builds).
# AppImage runs in its own continue-on-error step below so its
# persistent linuxdeploy failure (#6127 onwards) does not tank uploads.
args: ${{ matrix.platform == 'ubuntu-22.04' && (inputs.minimal && '--bundles deb' || '--bundles deb,rpm') || matrix.args }}
args: >-
${{ matrix.platform == 'ubuntu-22.04' && (inputs.minimal && '--bundles deb' || '--bundles deb,rpm') || matrix.args }}
--config '{"bundle":{"createUpdaterArtifacts":false}}'
# AppImage is decoupled so its linuxdeploy run gets a fresh process
# (rpm scratch state torn down) and its failure can't tank deb/rpm.
@@ -613,12 +615,19 @@ jobs:
pr-comment:
needs: build
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' && needs.build.result == 'success'
# Fork and Dependabot pull_request runs receive a read-only GITHUB_TOKEN,
# so the API cannot create or update PR comments there. The artifacts are
# still uploaded and remain available from the Actions run page.
if: >-
github.event_name == 'pull_request' &&
needs.build.result == 'success' &&
!github.event.pull_request.head.repo.fork &&
github.actor != 'dependabot[bot]'
permissions:
pull-requests: write
steps:
- name: Harden the runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
@@ -709,7 +718,7 @@ jobs:
if: always()
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
+5 -5
View File
@@ -53,7 +53,7 @@ jobs:
cache-scope: stirling-pdf-fat
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
@@ -61,7 +61,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -115,7 +115,7 @@ jobs:
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Build base image locally (PR base change only)
if: github.event_name == 'pull_request' && inputs.docker-base-changed == 'true'
@@ -195,7 +195,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
@@ -207,7 +207,7 @@ jobs:
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Build docker/unoserver/Dockerfile
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
+7 -7
View File
@@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
@@ -52,7 +52,7 @@ jobs:
DISABLE_ADDITIONAL_FEATURES: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Get version number
id: versionNumber
@@ -61,7 +61,7 @@ jobs:
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
- name: Login to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}
@@ -131,14 +131,14 @@ jobs:
frontend: ${{ steps.changes.outputs.frontend }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Check for file changes
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
id: changes
with:
filters: ".github/config/.files.yaml"
@@ -149,7 +149,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
@@ -185,7 +185,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
+18
View File
@@ -263,6 +263,16 @@ tasks:
- task: lint:eslint
- task: lint:dpdm
- task: lint:colors
- task: lint:css
lint:css:
desc: "Lint stylesheets for duplicate selectors"
deps: [install]
cmds:
# Covers the whole editor tree, including the portal/processor layer and
# public/css. Vendored CSS and build output are excluded via ignoreFiles
# in stylelint.config.mjs.
- npx stylelint "editor/**/*.css"
lint:colors:
desc: "Enforce theme tokens — no hardcoded colours or raw primitives in components"
@@ -388,6 +398,13 @@ tasks:
- task: typecheck:_run
vars: { PROJECT: editor/src/portal/tsconfig.json }
typecheck:storybook:
desc: "Typecheck Storybook config and stories"
deps: [prepare]
cmds:
- task: typecheck:_run
vars: { PROJECT: .storybook/tsconfig.json }
typecheck:all:
desc: "Typecheck all build variants"
cmds:
@@ -399,6 +416,7 @@ tasks:
- task: typecheck:scripts
- task: typecheck:prototypes
- task: typecheck:portal
- task: typecheck:storybook
# ============================================================
# Quality Gate
@@ -338,6 +338,7 @@ public class EndpointConfiguration {
addEndpointToGroup("PageOps", "split-pages");
addEndpointToGroup("PageOps", "rearrange-pages");
addEndpointToGroup("PageOps", "rotate-pdf");
addEndpointToGroup("PageOps", "auto-rotate-pdf");
addEndpointToGroup("PageOps", "multi-page-layout");
addEndpointToGroup("PageOps", "booklet-imposition");
addEndpointToGroup("PageOps", "scale-pages");
@@ -1076,6 +1076,20 @@ public class ApplicationProperties {
private Quotas quotas = new Quotas();
private Sharing sharing = new Sharing();
private Signing signing = new Signing();
private Encryption encryption = new Encryption();
/**
* Encryption at rest for stored files (Pro/Enterprise). Enabling encrypts new writes;
* disabling later only stops encrypting new writes — existing encrypted files keep
* decrypting as long as the key material is present. The master key is resolved like the
* credential key: {@code stirling.security.fileEncryptionKey} property, {@code
* STIRLING_FILE_ENCRYPTION_KEY} env var, or an auto-generated {@code file-encryption.key}
* in the config directory.
*/
@Data
public static class Encryption {
private boolean enabled = false;
}
@Data
public static class Local {
+1 -1
View File
@@ -50,7 +50,7 @@ dependencies {
implementation "org.apache.pdfbox:preflight:$pdfboxVersion"
implementation "org.apache.pdfbox:xmpbox:$pdfboxVersion"
implementation 'org.verapdf:validation-model:1.28.2'
implementation 'org.verapdf:validation-model:1.30.2'
// CVE-2025-66453: Explicit rhino 1.7.15 to override verapdf's 1.7.13
implementation "org.mozilla:rhino:${rhinoVersion}"
@@ -0,0 +1,385 @@
package stirling.software.SPDF.controller.api.misc;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import javax.imageio.ImageIO;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.EndpointConfiguration;
import stirling.software.SPDF.model.api.misc.AutoRotateAnalysisResult;
import stirling.software.SPDF.model.api.misc.AutoRotateAnalysisResult.PageResult;
import stirling.software.SPDF.model.api.misc.AutoRotatePdfRequest;
import stirling.software.SPDF.model.api.misc.PageRotation;
import stirling.software.SPDF.utils.AutoRotateDetection;
import stirling.software.SPDF.utils.AutoRotateDetection.OsdResult;
import stirling.software.SPDF.utils.AutoRotateDetection.TextDirection;
import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.MiscApi;
import stirling.software.common.configuration.RuntimePathConfig;
import stirling.software.common.enumeration.ResourceWeight;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
import stirling.software.common.util.TempDirectory;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@MiscApi
@Slf4j
@RequiredArgsConstructor
public class AutoRotateController {
// OSD decides orientation from script shape, not character identity, so it does not need
// OCR-grade resolution. 150 DPI is ample for that and a quarter of the pixels of 300.
private static final int OSD_RENDER_DPI = 150;
private static final String METHOD_TEXT = "text";
private static final String METHOD_OSD = "osd";
private static final String METHOD_INFERRED = "inferred";
private static final String METHOD_NONE = "none";
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
private final EndpointConfiguration endpointConfiguration;
private final RuntimePathConfig runtimePathConfig;
private final ApplicationProperties applicationProperties;
@AutoJobPostMapping(
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
value = "/auto-rotate-pdf",
resourceWeight = ResourceWeight.LARGE_WEIGHT)
@Operation(
summary = "Detect and fix the orientation of every page",
description =
"Detects each page's orientation (embedded-text direction first, Tesseract OSD"
+ " for scanned pages) and sets the page rotation so the content"
+ " displays upright. With dryRun=true, returns a JSON per-page report"
+ " instead of the PDF. With pageRotations set, applies the given"
+ " corrections without running detection."
+ " Input:PDF Output:PDF Type:SISO")
public ResponseEntity<?> autoRotatePdf(@Valid @ModelAttribute AutoRotatePdfRequest request)
throws IOException, InterruptedException {
String mode =
request.getDetectionMode() == null
? "auto"
: request.getDetectionMode().toLowerCase(Locale.ROOT);
if (!"auto".equals(mode) && !"text".equals(mode) && !"osd".equals(mode)) {
throw ExceptionUtils.createIllegalArgumentException(
ExceptionUtils.ErrorCode.INVALID_ARGUMENT,
"detectionMode",
"must be one of auto, text, osd");
}
try (PDDocument document = pdfDocumentFactory.load(request)) {
if (request.getPageRotations() != null && !request.getPageRotations().isEmpty()) {
applyExplicitRotations(document, request.getPageRotations());
return pdfResponse(document, request);
}
AutoRotateAnalysisResult analysis = analyse(document, mode, request);
if (request.isDryRun()) {
return ResponseEntity.ok(analysis);
}
for (PageResult pageResult : analysis.getPages()) {
if (pageResult.isApply()) {
PDPage page = document.getPage(pageResult.getPageNumber() - 1);
page.setRotation(
Math.floorMod(page.getRotation() + pageResult.getCorrection(), 360));
}
}
return pdfResponse(document, request);
}
}
private AutoRotateAnalysisResult analyse(
PDDocument document, String mode, AutoRotatePdfRequest request)
throws IOException, InterruptedException {
double threshold =
request.getConfidenceThreshold() == null ? 14.0 : request.getConfidenceThreshold();
boolean tesseractAvailable = endpointConfiguration.isGroupEnabled("tesseract");
boolean useText = !"osd".equals(mode);
boolean useOsd = !"text".equals(mode);
List<PageResult> results = new ArrayList<>();
List<Integer> osdCandidates = new ArrayList<>();
int pageCount = document.getNumberOfPages();
// One walk of the document for all pages, rather than one walk per page.
List<TextDirection> textDirections =
useText ? AutoRotateDetection.detectTextDirections(document) : List.of();
for (int i = 0; i < pageCount; i++) {
int currentRotation = Math.floorMod(document.getPage(i).getRotation(), 360);
PageResult result =
PageResult.builder()
.pageNumber(i + 1)
.currentRotation(currentRotation)
.method(METHOD_NONE)
.build();
if (useText) {
TextDirection direction = textDirections.get(i);
if (direction.isConclusive()) {
int correction =
AutoRotateDetection.correctionFromTextDirection(
direction.dominantDirection(), currentRotation);
result.setMethod(METHOD_TEXT);
result.setCorrection(correction);
result.setConfidence(direction.dominance() * 100);
result.setApply(correction != 0);
} else if (!useOsd) {
result.setNote(
direction.glyphCount() < AutoRotateDetection.MIN_GLYPHS
? "tooFewGlyphs"
: "noDominantDirection");
}
}
if (useOsd && METHOD_NONE.equals(result.getMethod())) {
if (tesseractAvailable) {
osdCandidates.add(i);
} else {
result.setNote("tesseractUnavailable");
}
}
results.add(result);
}
if (!osdCandidates.isEmpty()) {
runOsdOnPages(document, osdCandidates, results, threshold);
}
if (request.isInferUndetected()) {
inferUndetectedPages(results);
}
return summarise(results, pageCount);
}
/**
* Fill in pages that no signal could decide, using the pages that could. When every decided
* page sharing an undecided page's current rotation agrees on one correction, that correction
* is the document's consensus for that rotation and is applied to the undecided page. This is
* the common "whole document rotated uniformly, but a cover or near-blank page has too little
* text to detect on its own" case. If decided pages disagree, nothing is inferred.
*/
private void inferUndetectedPages(List<PageResult> results) {
// rotation -> the single agreed correction, or null once a conflict is seen
Map<Integer, Integer> consensus = new HashMap<>();
Set<Integer> conflicted = new HashSet<>();
for (PageResult result : results) {
if (METHOD_NONE.equals(result.getMethod())) {
continue;
}
int rotation = result.getCurrentRotation();
if (conflicted.contains(rotation)) {
continue;
}
Integer existing = consensus.get(rotation);
if (existing == null) {
consensus.put(rotation, result.getCorrection());
} else if (existing != result.getCorrection()) {
conflicted.add(rotation);
consensus.remove(rotation);
}
}
for (PageResult result : results) {
if (!METHOD_NONE.equals(result.getMethod())) {
continue;
}
Integer correction = consensus.get(result.getCurrentRotation());
if (correction == null) {
continue;
}
result.setMethod(METHOD_INFERRED);
result.setCorrection(correction);
result.setConfidence(null);
result.setApply(correction != 0);
result.setNote("inferredFromDocument");
}
}
private void runOsdOnPages(
PDDocument document,
List<Integer> pageIndexes,
List<PageResult> results,
double threshold)
throws IOException, InterruptedException {
String tessDataPath = runtimePathConfig.getTessDataPath();
boolean haveOsdData =
tessDataPath != null && new File(tessDataPath, "osd.traineddata").exists();
int dpi = OSD_RENDER_DPI;
if (applicationProperties != null && applicationProperties.getSystem() != null) {
dpi = Math.min(OSD_RENDER_DPI, applicationProperties.getSystem().getMaxDPI());
}
final int renderDpi = dpi;
try (TempDirectory tempDir = new TempDirectory(tempFileManager)) {
PDFRenderer renderer = new PDFRenderer(document);
renderer.setSubsamplingAllowed(true);
// One reused path, deleted after every page: the images are throwaway input for
// Tesseract, so a long document must not accumulate one file per page on disk.
File imageFile = new File(tempDir.getPath().toFile(), "osd-page.bmp");
for (int pageIndex : pageIndexes) {
PageResult result = results.get(pageIndex);
try {
// Rendering honours the page's current /Rotate, so OSD sees the page exactly
// as a viewer would and its verdict is always an additive correction.
BufferedImage image =
ExceptionUtils.handleOomRendering(
pageIndex + 1,
renderDpi,
() ->
renderer.renderImageWithDPI(
pageIndex, renderDpi, ImageType.GRAY));
if (AutoRotateDetection.isBlankRender(image)) {
// Nothing for OSD to read; skip the process spawn entirely.
result.setNote("blankPage");
continue;
}
// BMP, not PNG: the file is deleted straight after Tesseract reads it, so
// paying for compression only to discard the result is wasted work.
ImageIO.write(image, "bmp", imageFile);
List<String> command = new ArrayList<>();
command.add("tesseract");
command.add(imageFile.getAbsolutePath());
command.add("stdout");
command.add("--psm");
command.add("0");
if (haveOsdData) {
command.add("--tessdata-dir");
command.add(tessDataPath);
}
ProcessExecutorResult processResult =
ProcessExecutor.getInstance(ProcessExecutor.Processes.TESSERACT)
.runCommandWithOutputHandling(command);
Optional<OsdResult> osd =
AutoRotateDetection.parseOsd(processResult.getMessages());
if (osd.isEmpty()) {
result.setNote("osdNoVerdict");
continue;
}
result.setConfidence(osd.get().confidence());
result.setCorrection(osd.get().rotate());
if (osd.get().confidence() >= threshold) {
result.setMethod(METHOD_OSD);
result.setApply(osd.get().rotate() != 0);
} else {
result.setNote("belowThreshold");
}
} catch (IOException e) {
// Textless pages make Tesseract exit non-zero; skip, never guess.
log.debug("OSD failed for page {}: {}", pageIndex + 1, e.getMessage());
result.setNote("osdFailed");
} finally {
Files.deleteIfExists(imageFile.toPath());
}
}
}
}
private void applyExplicitRotations(PDDocument document, List<PageRotation> rotations) {
int pageCount = document.getNumberOfPages();
Set<Integer> seen = new HashSet<>();
for (PageRotation entry : rotations) {
Integer pageNumber = entry.getPageNumber();
Integer angle = entry.getRotation();
if (pageNumber == null
|| angle == null
|| pageNumber < 1
|| pageNumber > pageCount
|| angle % 90 != 0) {
throw ExceptionUtils.createIllegalArgumentException(
ExceptionUtils.ErrorCode.INVALID_ARGUMENT,
"pageRotations",
"page numbers must exist and rotations must be multiples of 90");
}
// Rotations are additive, so a repeated page would be turned twice; reject rather
// than silently pick a winner.
if (!seen.add(pageNumber)) {
throw ExceptionUtils.createIllegalArgumentException(
ExceptionUtils.ErrorCode.INVALID_ARGUMENT,
"pageRotations",
"page " + pageNumber + " is listed more than once");
}
PDPage page = document.getPage(pageNumber - 1);
page.setRotation(Math.floorMod(page.getRotation() + angle, 360));
}
}
private AutoRotateAnalysisResult summarise(List<PageResult> results, int pageCount) {
int toRotate = 0;
int byText = 0;
int byOsd = 0;
int byInference = 0;
int undetected = 0;
for (PageResult result : results) {
if (result.isApply()) {
toRotate++;
}
switch (result.getMethod()) {
case METHOD_TEXT -> byText++;
case METHOD_OSD -> byOsd++;
case METHOD_INFERRED -> byInference++;
default -> undetected++;
}
}
return AutoRotateAnalysisResult.builder()
.pages(results)
.totalPages(pageCount)
.pagesToRotate(toRotate)
.detectedByText(byText)
.detectedByOsd(byOsd)
.inferred(byInference)
.undetected(undetected)
.build();
}
private ResponseEntity<?> pdfResponse(PDDocument document, AutoRotatePdfRequest request)
throws IOException {
String originalName =
request.getFileInput() != null
? request.getFileInput().getOriginalFilename()
: "document.pdf";
return WebResponseUtils.pdfDocToWebResponse(
document,
GeneralUtils.generateFilename(originalName, "_auto_rotated.pdf"),
tempFileManager);
}
}
@@ -140,11 +140,15 @@ public class ValidateSignatureController {
// covers the TSTInfo, which can never match.
boolean isDocTimeStamp = SUBFILTER_RFC3161.equals(sig.getSubFilter());
CMSSignedData signedData;
// Parse from a stream: /Contents is zero-padded to its reserved length and the
// byte[] constructors reject those trailing bytes since BC 1.85.
if (isDocTimeStamp) {
signedData = new CMSSignedData(signatureBytes);
signedData = new CMSSignedData(new ByteArrayInputStream(signatureBytes));
} else {
CMSProcessable content = new CMSProcessableByteArray(signedContent);
signedData = new CMSSignedData(content, signatureBytes);
signedData =
new CMSSignedData(
content, new ByteArrayInputStream(signatureBytes));
}
// What actually binds a timestamp to this document: the TSTInfo's message
@@ -0,0 +1,73 @@
package stirling.software.SPDF.model.api.misc;
import java.util.List;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/** Per-page orientation report returned by auto-rotate-pdf when dryRun is set. */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AutoRotateAnalysisResult {
private List<PageResult> pages;
private int totalPages;
@Schema(description = "Number of pages a correction would be applied to")
private int pagesToRotate;
private int detectedByText;
private int detectedByOsd;
@Schema(description = "Pages whose correction was inherited from the document consensus")
private int inferred;
private int undetected;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class PageResult {
@Schema(description = "1-based page number")
private int pageNumber;
@Schema(description = "The page's current /Rotate value, normalised to 0-270")
private int currentRotation;
@Schema(
description =
"Detected additional clockwise rotation that would make the page upright."
+ " Reported for diagnostics even when it is not used (for example"
+ " an OSD verdict below the confidence threshold); 'apply' is the"
+ " authority on whether it is actually applied")
private int correction;
@Schema(
description =
"Detection confidence: percentage of glyphs sharing the dominant direction"
+ " for method 'text', Tesseract orientation confidence for method"
+ " 'osd', absent when nothing was detected")
private Double confidence;
@Schema(
description = "How the orientation was determined",
allowableValues = {"text", "osd", "inferred", "none"})
private String method;
@Schema(description = "Whether the correction will be (or was) applied")
private boolean apply;
@Schema(description = "Machine-readable reason when no correction is applied")
private String note;
}
}
@@ -0,0 +1,58 @@
package stirling.software.SPDF.model.api.misc;
import java.util.List;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import lombok.Data;
import lombok.EqualsAndHashCode;
import stirling.software.common.model.api.PDFFile;
@Data
@EqualsAndHashCode(callSuper = true)
public class AutoRotatePdfRequest extends PDFFile {
@Schema(
description =
"Detection method. 'auto' tries embedded-text direction first and falls back"
+ " to Tesseract OSD for pages without usable text; 'text' uses only"
+ " embedded-text direction; 'osd' forces Tesseract OSD for every page",
allowableValues = {"auto", "text", "osd"},
defaultValue = "auto")
private String detectionMode = "auto";
@Schema(
description =
"Minimum Tesseract OSD orientation confidence required before a correction is"
+ " applied. Matches OCRmyPDF's --rotate-pages-threshold scale",
minimum = "0",
defaultValue = "14.0")
@Min(value = 0, message = "Confidence threshold must be non-negative")
private Double confidenceThreshold = 14.0;
@Schema(
description =
"If true, no rotation is applied; returns a JSON report of the per-page"
+ " detection results instead of a PDF")
private boolean dryRun;
@Schema(
description =
"When a page cannot be decided on its own but the pages that could be decided"
+ " agree on a single correction for that same current rotation, apply"
+ " that shared correction to the undecided page. Handles documents"
+ " rotated uniformly where some pages are too sparse to detect alone",
defaultValue = "true")
private boolean inferUndetected = true;
@Schema(
description =
"Optional pre-computed corrections to apply without running detection. Pages"
+ " not listed are left unchanged, and a page may only appear once")
@Valid
private List<PageRotation> pageRotations;
}
@@ -0,0 +1,28 @@
package stirling.software.SPDF.model.api.misc;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/** A pre-computed rotation for one page, used by auto-rotate-pdf's apply-only path. */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageRotation {
@Schema(
description = "1-based page number to rotate",
requiredMode = Schema.RequiredMode.REQUIRED,
example = "1")
private Integer pageNumber;
@Schema(
description =
"Additional clockwise rotation to add to the page's current rotation, in"
+ " degrees. Must be a multiple of 90",
requiredMode = Schema.RequiredMode.REQUIRED,
example = "90")
private Integer rotation;
}
@@ -200,8 +200,8 @@ public class VeraPDFService {
try (PDFAParser detectionParser =
Foundries.defaultInstance().createParser(new ByteArrayInputStream(pdfBytes))) {
declaredFlavour = detectionParser.getFlavour();
detectedFlavours = detectionParser.getFlavours();
detectedFlavours = detectedFlavours(detectionParser);
declaredFlavour = firstFlavour(detectedFlavours);
}
// For PDF/A flavours, we need to validate first to check if PDF/A identification exists in
@@ -299,7 +299,7 @@ public class VeraPDFService {
Foundries.defaultInstance()
.createParser(new ByteArrayInputStream(pdfBytes), flavour)) {
PDFAFlavour parserDeclared = parser.getFlavour();
PDFAFlavour parserDeclared = firstFlavour(detectedFlavours(parser));
PDFAValidator validator =
Foundries.defaultInstance().createValidator(flavour, false);
ValidationResult result = validator.validate(parser);
@@ -322,7 +322,19 @@ public class VeraPDFService {
}
private static boolean isPdfaFlavour(PDFAFlavour flavour) {
return PDFFlavours.isFlavourFamily(flavour, PDFAFlavour.SpecificationFamily.PDF_A);
return flavour != null
&& PDFFlavours.isFlavourFamily(flavour, PDFAFlavour.SpecificationFamily.PDF_A);
}
// veraPDF 1.30+ returns an empty flavour list for non-PDF/A files, where getFlavour() throws
private static List<PDFAFlavour> detectedFlavours(PDFAParser parser) {
List<PDFAFlavour> flavours = parser.getFlavours();
return flavours != null ? flavours : List.of();
}
// null means "no PDF/A flavour detected" rather than an error
private static PDFAFlavour firstFlavour(List<PDFAFlavour> flavours) {
return flavours.isEmpty() ? null : flavours.get(0);
}
private static String formatStandardDisplay(
@@ -0,0 +1,189 @@
package stirling.software.SPDF.utils;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.pdfbox.text.TextPosition;
/**
* Page-orientation detection primitives for the auto-rotate tool.
*
* <p>Two independent signals are supported: the dominant direction of a page's embedded text
* (cheap, digital PDFs only) and Tesseract's orientation-and-script-detection output (works on
* scans, requires the external binary). Both express their result as the additional clockwise
* /Rotate correction that makes the page display upright.
*/
public final class AutoRotateDetection {
private AutoRotateDetection() {}
/** Glyphs needed to trust the text signal at the ordinary dominance bar. */
public static final int MIN_GLYPHS = 30;
/** Fraction of glyphs that must share one direction at the ordinary bar. */
public static final double MIN_DOMINANCE = 0.95;
/**
* Glyphs needed to trust the text signal when the glyphs are near-unanimous. Lets sparse pages
* (a header, a single line, a rotated URL) be decided from their own text instead of falling
* through to OSD, as long as effectively every glyph agrees on the direction.
*/
public static final int MIN_GLYPHS_UNANIMOUS = 8;
/** Dominance required for the sparse-page path — essentially total agreement. */
public static final double UNANIMOUS_DOMINANCE = 0.99;
/**
* Dominant embedded-text direction of one page.
*
* @param dominantDirection glyph direction in page space, degrees CCW (0/90/180/270)
* @param dominance fraction of counted glyphs sharing the dominant direction (0..1)
* @param glyphCount number of non-whitespace glyphs counted
*/
public record TextDirection(int dominantDirection, double dominance, int glyphCount) {
public boolean isConclusive() {
if (glyphCount >= MIN_GLYPHS && dominance >= MIN_DOMINANCE) {
return true;
}
return glyphCount >= MIN_GLYPHS_UNANIMOUS && dominance >= UNANIMOUS_DOMINANCE;
}
}
/**
* Parsed Tesseract OSD verdict.
*
* @param rotate clockwise degrees to rotate the rendered page so text is upright
* @param confidence Tesseract's orientation confidence (same scale OCRmyPDF thresholds on)
*/
public record OsdResult(int rotate, double confidence) {}
private static final Pattern OSD_ROTATE =
Pattern.compile("^Rotate:\\s*(\\d+)", Pattern.MULTILINE);
private static final Pattern OSD_CONFIDENCE =
Pattern.compile("^Orientation confidence:\\s*([0-9.]+)", Pattern.MULTILINE);
/**
* Counts non-whitespace glyph directions for every page in one pass. A stripper per page would
* re-walk the document once per page, which is quadratic on long documents; this walks it once
* and buckets glyphs by the page being processed.
*
* @return one entry per page, in page order
*/
public static List<TextDirection> detectTextDirections(PDDocument document) throws IOException {
int pageCount = document.getNumberOfPages();
DirectionCountingStripper stripper = new DirectionCountingStripper(pageCount);
stripper.setStartPage(1);
stripper.setEndPage(pageCount);
stripper.getText(document);
List<TextDirection> directions = new ArrayList<>(pageCount);
for (int page = 0; page < pageCount; page++) {
int[] counts = stripper.counts[page];
int total = 0;
int bestIndex = 0;
for (int i = 0; i < 4; i++) {
total += counts[i];
if (counts[i] > counts[bestIndex]) {
bestIndex = i;
}
}
double dominance = total == 0 ? 0 : (double) counts[bestIndex] / total;
directions.add(new TextDirection(bestIndex * 90, dominance, total));
}
return directions;
}
/**
* True when a rendered page carries no ink worth analysing. Checked after rendering but before
* spawning Tesseract, since the process spawn costs far more than the pixel scan and this
* catches both empty generated pages and scanned blanks (the back of a duplex sheet).
*/
public static boolean isBlankRender(BufferedImage image) {
final int darkThreshold = 200; // 8-bit grey; anything lighter counts as paper
final int step = 4; // subsample: blank pages are uniform, no need for every pixel
long sampled = 0;
long dark = 0;
for (int y = 0; y < image.getHeight(); y += step) {
for (int x = 0; x < image.getWidth(); x += step) {
sampled++;
if ((image.getRGB(x, y) & 0xFF) < darkThreshold) {
dark++;
// A page needs a meaningful amount of ink before OSD can do anything;
// bail out as soon as we know there is enough.
if (dark > sampled / 1000 + 20) {
return false;
}
}
}
}
return true;
}
/**
* Clockwise /Rotate correction for a page whose dominant glyph direction (page space, CCW) is
* {@code dominantDirection} and whose current /Rotate is {@code pageRotation}. Derivation: the
* on-screen text angle is (direction - rotation) CCW, and adding d to /Rotate turns the display
* a further d clockwise, so the correction that zeroes the screen angle is their difference.
*/
public static int correctionFromTextDirection(int dominantDirection, int pageRotation) {
return Math.floorMod(dominantDirection - pageRotation, 360);
}
/** Extracts rotation and confidence from `tesseract <img> stdout --psm 0` output. */
public static Optional<OsdResult> parseOsd(String tesseractOutput) {
if (tesseractOutput == null) {
return Optional.empty();
}
Matcher rotate = OSD_ROTATE.matcher(tesseractOutput);
Matcher confidence = OSD_CONFIDENCE.matcher(tesseractOutput);
if (!rotate.find() || !confidence.find()) {
return Optional.empty();
}
try {
return Optional.of(
new OsdResult(
Integer.parseInt(rotate.group(1)),
Double.parseDouble(confidence.group(1))));
} catch (NumberFormatException e) {
return Optional.empty();
}
}
private static class DirectionCountingStripper extends PDFTextStripper {
// counts[page][i] holds glyphs on that page whose direction is i * 90 degrees
final int[][] counts;
DirectionCountingStripper(int pageCount) throws IOException {
this.counts = new int[pageCount][4];
}
/**
* PDFBox snaps glyph direction to a quadrant, so getDir() only ever yields 0/90/180/270 —
* obliquely drawn text (30, 45, 135 degrees) is reported as 0 rather than as its true
* angle. Skew is therefore invisible to this signal by construction, which is consistent
* with skew being out of scope here: only 90-degree orientation is corrected.
*/
@Override
protected void processTextPosition(TextPosition text) {
String unicode = text.getUnicode();
if (unicode == null || unicode.isBlank()) {
return;
}
int page = getCurrentPageNo() - 1;
if (page < 0 || page >= counts.length) {
return;
}
counts[page][Math.floorMod(Math.round(text.getDir()), 360) / 90]++;
// super is intentionally not called: we only count, no text assembly needed
}
}
}
@@ -292,6 +292,26 @@ storage:
linkExpirationDays: 3 # Number of days before share links expire
signing:
enabled: false # set to 'true' to enable group signing workflow (requires storage.enabled) [ALPHA]
# ====================================================================================
# ENCRYPTION AT REST - PRO / ENTERPRISE LICENSE REQUIRED TO ENABLE
# ====================================================================================
# Encrypts stored files (AES-256 envelope encryption, per-team keys). The master key is
# resolved in this order:
# 1. stirling.security.fileEncryptionKey property
# 2. STIRLING_FILE_ENCRYPTION_KEY environment variable
# 3. an auto-generated configs/file-encryption.key (single-node only; cluster mode
# requires an explicitly shared key on every node)
# Generate a key with: openssl rand -base64 32
#
# *** BACK UP THE MASTER KEY. Losing it makes every encrypted stored file ***
# *** permanently unrecoverable. Verify backups against the key fingerprint logged ***
# *** at startup. ***
#
# Enabling encrypts new writes only (existing files stay readable as plaintext).
# Disabling later only stops encrypting new writes - existing encrypted files remain
# readable as long as the key material is present.
encryption:
enabled: false # set to 'true' to encrypt stored files at rest
userListScope: org # Signing user-picker scope: 'org' (default) = whole instance, else caller's team only.
autoPipeline:
outputFolder: "" # Output folder for processed pipeline files (leave empty for default)
@@ -0,0 +1,342 @@
package stirling.software.SPDF.controller.api.misc;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.List;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.pdfbox.text.TextPosition;
import org.apache.pdfbox.util.Matrix;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import stirling.software.SPDF.config.EndpointConfiguration;
import stirling.software.SPDF.model.api.misc.AutoRotateAnalysisResult;
import stirling.software.SPDF.model.api.misc.AutoRotatePdfRequest;
import stirling.software.SPDF.model.api.misc.PageRotation;
import stirling.software.common.configuration.RuntimePathConfig;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
@ExtendWith(MockitoExtension.class)
class AutoRotateControllerTest {
private static final String SAMPLE_TEXT =
"The quick brown fox jumps over the lazy dog again and again";
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock private TempFileManager tempFileManager;
@Mock private EndpointConfiguration endpointConfiguration;
@Mock private RuntimePathConfig runtimePathConfig;
@Mock private ApplicationProperties applicationProperties;
@InjectMocks private AutoRotateController controller;
@BeforeEach
void setUp() throws Exception {
lenient()
.when(tempFileManager.createManagedTempFile(anyString()))
.thenAnswer(
inv -> {
File f =
Files.createTempFile("test", inv.<String>getArgument(0))
.toFile();
TempFile tf = mock(TempFile.class);
lenient().when(tf.getFile()).thenReturn(f);
lenient().when(tf.getPath()).thenReturn(f.toPath());
return tf;
});
lenient().when(endpointConfiguration.isGroupEnabled("tesseract")).thenReturn(false);
}
private static PDDocument docWithUprightText(int... pageRotations) throws IOException {
PDDocument document = new PDDocument();
for (int rotation : pageRotations) {
PDPage page = new PDPage(PDRectangle.LETTER);
document.addPage(page);
try (PDPageContentStream content = new PDPageContentStream(document, page)) {
content.beginText();
content.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
content.setTextMatrix(Matrix.getTranslateInstance(72, 400));
content.showText(SAMPLE_TEXT);
content.endText();
}
page.setRotation(rotation);
}
return document;
}
private AutoRotatePdfRequest request(PDDocument document) throws IOException {
AutoRotatePdfRequest request = new AutoRotatePdfRequest();
request.setFileInput(
new MockMultipartFile(
"fileInput",
"test.pdf",
MediaType.APPLICATION_PDF_VALUE,
new byte[] {1, 2, 3}));
when(pdfDocumentFactory.load(request)).thenReturn(document);
return request;
}
private static PDDocument reload(ResponseEntity<?> response) throws IOException {
Resource resource = (Resource) response.getBody();
return Loader.loadPDF(resource.getContentAsByteArray());
}
private static PDDocument docWithTextAt(int textAngleDegrees, int pageRotation)
throws IOException {
PDDocument document = new PDDocument();
PDPage page = new PDPage(PDRectangle.LETTER);
document.addPage(page);
try (PDPageContentStream content = new PDPageContentStream(document, page)) {
content.beginText();
content.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
content.setTextMatrix(
Matrix.getRotateInstance(Math.toRadians(textAngleDegrees), 300, 400));
content.showText(SAMPLE_TEXT);
content.endText();
}
page.setRotation(pageRotation);
return document;
}
/**
* Reads the dominant glyph direction straight from a document, independently of the production
* detection code, so the round-trip assertion below validates the result rather than restating
* the formula under test.
*/
private static int dominantGlyphDirection(PDDocument document) throws IOException {
int[] counts = new int[4];
PDFTextStripper stripper =
new PDFTextStripper() {
@Override
protected void processTextPosition(TextPosition text) {
if (!text.getUnicode().isBlank()) {
counts[Math.floorMod(Math.round(text.getDir()), 360) / 90]++;
}
}
};
stripper.setStartPage(1);
stripper.setEndPage(1);
stripper.getText(document);
int best = 0;
for (int i = 1; i < 4; i++) {
if (counts[i] > counts[best]) {
best = i;
}
}
return best * 90;
}
/**
* End-to-end round trip: build a page whose text is drawn at a known angle under a known
* /Rotate, run the real controller, then assert the output actually displays upright. Upright
* means the glyph direction and the page rotation cancel — computed here in the test, not via
* the production helper.
*/
@ParameterizedTest
@CsvSource({
"0, 0",
"0, 90",
"0, 180",
"0, 270",
"90, 0",
"90, 90",
"180, 0",
"180, 270",
"270, 90",
})
void roundTripLeavesPageUpright(int textAngle, int pageRotation) throws Exception {
AutoRotatePdfRequest request = request(docWithTextAt(textAngle, pageRotation));
request.setDetectionMode("text");
ResponseEntity<?> response = controller.autoRotatePdf(request);
try (PDDocument corrected = reload(response)) {
int glyphDirection = dominantGlyphDirection(corrected);
int finalRotation = Math.floorMod(corrected.getPage(0).getRotation(), 360);
assertThat(Math.floorMod(glyphDirection - finalRotation, 360))
.as(
"text drawn at %d under /Rotate %d should display upright, got glyph"
+ " direction %d with /Rotate %d",
textAngle, pageRotation, glyphDirection, finalRotation)
.isZero();
}
}
@Test
void dryRunReportsTextDetection() throws Exception {
AutoRotatePdfRequest request = request(docWithUprightText(90, 0));
request.setDryRun(true);
request.setDetectionMode("text");
ResponseEntity<?> response = controller.autoRotatePdf(request);
AutoRotateAnalysisResult result = (AutoRotateAnalysisResult) response.getBody();
assertThat(result.getTotalPages()).isEqualTo(2);
assertThat(result.getPagesToRotate()).isEqualTo(1);
assertThat(result.getDetectedByText()).isEqualTo(2);
AutoRotateAnalysisResult.PageResult first = result.getPages().get(0);
assertThat(first.getMethod()).isEqualTo("text");
assertThat(first.getCorrection()).isEqualTo(270);
assertThat(first.isApply()).isTrue();
assertThat(first.getConfidence()).isEqualTo(100.0);
AutoRotateAnalysisResult.PageResult second = result.getPages().get(1);
assertThat(second.getCorrection()).isZero();
assertThat(second.isApply()).isFalse();
}
@Test
void appliesDetectedCorrections() throws Exception {
AutoRotatePdfRequest request = request(docWithUprightText(90, 0));
request.setDetectionMode("text");
ResponseEntity<?> response = controller.autoRotatePdf(request);
try (PDDocument corrected = reload(response)) {
assertThat(corrected.getPage(0).getRotation()).isZero();
assertThat(corrected.getPage(1).getRotation()).isZero();
}
}
@Test
void appliesExplicitPageRotations() throws Exception {
AutoRotatePdfRequest request = request(docWithUprightText(0, 0));
request.setPageRotations(List.of(new PageRotation(1, 90)));
ResponseEntity<?> response = controller.autoRotatePdf(request);
try (PDDocument corrected = reload(response)) {
assertThat(corrected.getPage(0).getRotation()).isEqualTo(90);
assertThat(corrected.getPage(1).getRotation()).isZero();
}
}
@Test
void reportsTesseractUnavailableForTextlessPages() throws Exception {
PDDocument document = new PDDocument();
document.addPage(new PDPage(PDRectangle.LETTER));
AutoRotatePdfRequest request = request(document);
request.setDryRun(true);
ResponseEntity<?> response = controller.autoRotatePdf(request);
AutoRotateAnalysisResult result = (AutoRotateAnalysisResult) response.getBody();
AutoRotateAnalysisResult.PageResult page = result.getPages().get(0);
assertThat(page.getMethod()).isEqualTo("none");
assertThat(page.getNote()).isEqualTo("tesseractUnavailable");
assertThat(result.getUndetected()).isEqualTo(1);
}
@Test
void infersUndetectedPageFromDocumentConsensus() throws Exception {
// Page 1 has body text and is rotated 90 (-> 270 correction); page 2 is blank and shares
// the same rotation. With OSD unavailable, page 2 can't be detected on its own, so it
// should inherit page 1's 270 correction.
PDDocument document = docWithUprightText(90);
PDPage blank = new PDPage(PDRectangle.LETTER);
blank.setRotation(90);
document.addPage(blank);
AutoRotatePdfRequest request = request(document);
request.setDryRun(true);
request.setDetectionMode("text");
ResponseEntity<?> response = controller.autoRotatePdf(request);
AutoRotateAnalysisResult result = (AutoRotateAnalysisResult) response.getBody();
AutoRotateAnalysisResult.PageResult page2 = result.getPages().get(1);
assertThat(page2.getMethod()).isEqualTo("inferred");
assertThat(page2.getCorrection()).isEqualTo(270);
assertThat(page2.isApply()).isTrue();
assertThat(page2.getNote()).isEqualTo("inferredFromDocument");
assertThat(result.getInferred()).isEqualTo(1);
assertThat(result.getPagesToRotate()).isEqualTo(2);
}
@Test
void doesNotInferWhenDisabled() throws Exception {
PDDocument document = docWithUprightText(90);
PDPage blank = new PDPage(PDRectangle.LETTER);
blank.setRotation(90);
document.addPage(blank);
AutoRotatePdfRequest request = request(document);
request.setDryRun(true);
request.setDetectionMode("text");
request.setInferUndetected(false);
ResponseEntity<?> response = controller.autoRotatePdf(request);
AutoRotateAnalysisResult result = (AutoRotateAnalysisResult) response.getBody();
AutoRotateAnalysisResult.PageResult page2 = result.getPages().get(1);
assertThat(page2.getMethod()).isEqualTo("none");
assertThat(page2.isApply()).isFalse();
assertThat(result.getInferred()).isZero();
assertThat(result.getUndetected()).isEqualTo(1);
}
@Test
void rejectsInvalidDetectionMode() {
AutoRotatePdfRequest request = new AutoRotatePdfRequest();
request.setDetectionMode("magic");
assertThatThrownBy(() -> controller.autoRotatePdf(request))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
void rejectsRotationThatIsNotAMultipleOf90() throws Exception {
AutoRotatePdfRequest request = request(docWithUprightText(0));
request.setPageRotations(List.of(new PageRotation(1, 45)));
assertThatThrownBy(() -> controller.autoRotatePdf(request))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
void rejectsPageRotationOutsideTheDocument() throws Exception {
AutoRotatePdfRequest request = request(docWithUprightText(0));
request.setPageRotations(List.of(new PageRotation(5, 90)));
assertThatThrownBy(() -> controller.autoRotatePdf(request))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
void rejectsDuplicatePageInRotations() throws Exception {
// Rotations are additive, so applying the same page twice would over-rotate it.
AutoRotatePdfRequest request = request(docWithUprightText(0, 0));
request.setPageRotations(List.of(new PageRotation(1, 90), new PageRotation(1, 90)));
assertThatThrownBy(() -> controller.autoRotatePdf(request))
.isInstanceOf(IllegalArgumentException.class);
}
}
@@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -291,6 +292,27 @@ class ValidateSignatureControllerMoreTest {
}
}
@Nested
@DisplayName("Padded signature contents")
class PaddedContentsTests {
@Test
@DisplayName("Zero-padded /Contents still parses as CMS")
void zeroPaddedContentsParsesAsCms() throws Exception {
byte[] contents;
try (PDDocument doc = Loader.loadPDF(signedPdfBytes)) {
PDSignature sig = doc.getSignatureDictionaries().get(0);
contents = sig.getContents(new ByteArrayInputStream(signedPdfBytes));
}
// /Contents is zero-filled up to its reserved length. Since BC 1.85 the byte[]
// constructors reject that tail ("Extra data detected in stream"), streams do not.
assertThat(contents[contents.length - 1]).isZero();
CMSSignedData parsed = new CMSSignedData(new ByteArrayInputStream(contents));
assertThat(parsed.getSignerInfos().size()).isEqualTo(1);
}
}
@Nested
@DisplayName("Error and edge handling")
class ErrorHandlingTests {
@@ -0,0 +1,197 @@
package stirling.software.SPDF.service;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.verapdf.gf.foundry.VeraGreenfieldFoundryProvider;
import org.verapdf.pdfa.Foundries;
import org.verapdf.pdfa.PDFAParser;
import org.verapdf.pdfa.PDFAValidator;
import org.verapdf.pdfa.flavours.PDFAFlavour;
import org.verapdf.pdfa.results.ValidationResult;
import stirling.software.SPDF.model.api.security.PDFVerificationResult;
/**
* Exercises {@link VeraPDFService} against real PDF/A files. Fixtures were produced by Ghostscript
* with the same flags as ConvertPDFToPDFA and independently confirmed conformant by veraPDF.
*/
class VeraPDFServicePdfaFixtureTest {
private static final String VALID_1B = "valid-pdfa-1b.pdf";
private static final String VALID_2B = "valid-pdfa-2b.pdf";
private static final String DECLARED_BUT_INVALID_1B = "declared-pdfa-1b-no-outputintent.pdf";
private VeraPDFService service;
@BeforeEach
void setUp() {
service = new VeraPDFService();
service.initialize();
}
@Test
void fixtures_areGenuinePdfaAccordingToVeraPdfItself() throws Exception {
assertVeraPdfVerdict(VALID_1B, PDFAFlavour.PDFA_1_B, true);
assertVeraPdfVerdict(VALID_2B, PDFAFlavour.PDFA_2_B, true);
assertVeraPdfVerdict(DECLARED_BUT_INVALID_1B, PDFAFlavour.PDFA_1_B, false);
}
@Test
void validatePDF_realPdfa1b_reportsCompliantPdfa1b() throws Exception {
PDFVerificationResult result = onlyResult(fixture(VALID_1B));
assertEquals("1b", result.getStandard());
assertEquals("1b", result.getValidationProfile());
assertTrue(result.isDeclaredPdfa(), "Genuine PDF/A-1b must be reported as declared PDF/A");
assertTrue(result.isCompliant(), "Genuine PDF/A-1b must validate as compliant");
assertEquals(
0, result.getTotalFailures(), () -> "Unexpected failures: " + messages(result));
assertTrue(
result.getStandardName().startsWith("PDF/A-"),
"Display name should name the PDF/A standard, got: " + result.getStandardName());
assertTrue(
result.getStandardName().endsWith(" compliant"),
"Display name should read as compliant, got: " + result.getStandardName());
assertEquals(result.getStandardName(), result.getComplianceSummary());
}
@Test
void validatePDF_realPdfa2b_reportsPdfa2bAndNotPdfa1b() throws Exception {
PDFVerificationResult result = onlyResult(fixture(VALID_2B));
// Proves firstFlavour() returns the flavour actually declared, not just a non-null one
assertEquals("2b", result.getStandard());
assertEquals("2b", result.getValidationProfile());
assertNotEquals("1b", result.getStandard());
assertTrue(result.isDeclaredPdfa());
assertTrue(result.isCompliant(), () -> "Unexpected failures: " + messages(result));
assertEquals(0, result.getTotalFailures());
}
@Test
void validatePDF_plainPdf_reportsNotPdfaAndDoesNotThrowIndexOutOfBounds() throws Exception {
byte[] pdfBytes = createSimplePdf();
// veraPDF 1.30 returns an empty flavour list here where 1.28 returned [1b]; get(0) threw
List<PDFVerificationResult> results =
assertDoesNotThrow(
() -> service.validatePDF(new ByteArrayInputStream(pdfBytes)),
"Empty veraPDF flavour list must not surface as IndexOutOfBoundsException");
assertEquals(1, results.size());
PDFVerificationResult result = results.get(0);
assertEquals("not-pdfa", result.getStandard());
assertFalse(result.isDeclaredPdfa());
assertFalse(result.isCompliant());
assertEquals("Not PDF/A (no PDF/A identification metadata)", result.getStandardName());
}
@Test
void validatePDF_declaresPdfaButNotConformant_reportsFlavourWithFailures() throws Exception {
PDFVerificationResult result = onlyResult(fixture(DECLARED_BUT_INVALID_1B));
// "declares PDF/A but broken" must stay distinct from "not PDF/A at all"
assertEquals("1b", result.getStandard());
assertNotEquals("not-pdfa", result.getStandard());
assertTrue(result.isDeclaredPdfa(), "XMP still declares pdfaid:part=1");
assertFalse(result.isCompliant(), "Stripped OutputIntent must fail conformance");
assertTrue(result.getTotalFailures() > 0, "Non-conformance must be reported as issues");
assertTrue(
result.getStandardName().endsWith(" with errors"),
"Display name should flag errors, got: " + result.getStandardName());
assertTrue(
messages(result).contains("OutputIntent"),
"Expected the missing OutputIntent to be reported, got: " + messages(result));
}
@Test
void firstFlavour_withEmptyList_returnsNullInsteadOfThrowing() throws Exception {
Method method = VeraPDFService.class.getDeclaredMethod("firstFlavour", List.class);
method.setAccessible(true);
assertNull(method.invoke(null, List.of()));
assertEquals(PDFAFlavour.PDFA_2_B, method.invoke(null, List.of(PDFAFlavour.PDFA_2_B)));
}
@Test
void detectedFlavours_withNullFlavourList_returnsEmptyList() throws Exception {
Method method =
VeraPDFService.class.getDeclaredMethod("detectedFlavours", PDFAParser.class);
method.setAccessible(true);
PDFAParser parser = mock(PDFAParser.class);
when(parser.getFlavours()).thenReturn(null);
assertEquals(List.of(), method.invoke(null, parser));
}
private static void assertVeraPdfVerdict(
String fixtureName, PDFAFlavour expectedFlavour, boolean expectedCompliant)
throws Exception {
VeraGreenfieldFoundryProvider.initialise();
byte[] bytes = fixture(fixtureName);
List<PDFAFlavour> flavours;
try (PDFAParser parser =
Foundries.defaultInstance().createParser(new ByteArrayInputStream(bytes))) {
flavours = parser.getFlavours();
}
assertEquals(List.of(expectedFlavour), flavours, fixtureName + " declared flavours");
try (PDFAParser parser =
Foundries.defaultInstance()
.createParser(new ByteArrayInputStream(bytes), expectedFlavour)) {
PDFAValidator validator =
Foundries.defaultInstance().createValidator(expectedFlavour, false);
ValidationResult result = validator.validate(parser);
assertEquals(
expectedCompliant, result.isCompliant(), fixtureName + " veraPDF compliance");
}
}
private PDFVerificationResult onlyResult(byte[] pdfBytes) throws Exception {
List<PDFVerificationResult> results =
service.validatePDF(new ByteArrayInputStream(pdfBytes));
assertNotNull(results);
assertEquals(1, results.size(), () -> "Expected a single result, got: " + results);
return results.get(0);
}
private static String messages(PDFVerificationResult result) {
StringBuilder builder = new StringBuilder();
for (PDFVerificationResult.ValidationIssue issue : result.getFailures()) {
builder.append(issue.getMessage()).append(" | ");
}
return builder.toString();
}
private static byte[] fixture(String name) throws IOException {
try (InputStream in =
VeraPDFServicePdfaFixtureTest.class.getResourceAsStream("/pdfa/" + name)) {
assertNotNull(in, "Missing test fixture /pdfa/" + name);
return in.readAllBytes();
}
}
private static byte[] createSimplePdf() throws IOException {
try (PDDocument document = new PDDocument()) {
document.addPage(new PDPage());
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
return baos.toByteArray();
}
}
}
@@ -0,0 +1,248 @@
package stirling.software.SPDF.utils;
import static org.assertj.core.api.Assertions.assertThat;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
import org.apache.pdfbox.util.Matrix;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import stirling.software.SPDF.utils.AutoRotateDetection.OsdResult;
import stirling.software.SPDF.utils.AutoRotateDetection.TextDirection;
class AutoRotateDetectionTest {
private static final String SAMPLE_TEXT =
"The quick brown fox jumps over the lazy dog again and again";
private PDDocument docWithText(int textAngleDegrees, int pageRotation) throws IOException {
PDDocument document = new PDDocument();
PDPage page = new PDPage(PDRectangle.LETTER);
document.addPage(page);
try (PDPageContentStream content = new PDPageContentStream(document, page)) {
content.beginText();
content.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
content.setTextMatrix(
Matrix.getRotateInstance(Math.toRadians(textAngleDegrees), 300, 400));
content.showText(SAMPLE_TEXT);
content.endText();
}
page.setRotation(pageRotation);
return document;
}
/**
* Ground truth per the PDF spec: /Rotate R displays the page R degrees clockwise, so upright
* page-space text under /Rotate R needs a further (360 - R) % 360 to display upright again;
* text drawn rotated T degrees CCW in page space needs T clockwise to correct. Combined, the
* expected correction is (T - R) mod 360.
*/
@ParameterizedTest
@CsvSource({
// textAngle, pageRotation, expectedCorrection
"0, 0, 0",
"0, 90, 270",
"0, 180, 180",
"0, 270, 90",
"90, 0, 90",
"180, 0, 180",
"270, 0, 270",
"90, 90, 0",
"180, 90, 90",
})
void detectsCorrectionForRotatedTextAndPages(
int textAngle, int pageRotation, int expectedCorrection) throws IOException {
try (PDDocument document = docWithText(textAngle, pageRotation)) {
TextDirection direction = AutoRotateDetection.detectTextDirections(document).get(0);
assertThat(direction.isConclusive())
.as(
"direction should be conclusive, glyphs=%d dominance=%s",
direction.glyphCount(), direction.dominance())
.isTrue();
assertThat(
AutoRotateDetection.correctionFromTextDirection(
direction.dominantDirection(),
Math.floorMod(pageRotation, 360)))
.isEqualTo(expectedCorrection);
}
}
@Test
void mixedDirectionsAreNotConclusive() throws IOException {
PDDocument document = new PDDocument();
PDPage page = new PDPage(PDRectangle.LETTER);
document.addPage(page);
try (PDPageContentStream content = new PDPageContentStream(document, page)) {
content.beginText();
content.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
content.setTextMatrix(Matrix.getTranslateInstance(100, 400));
content.showText(SAMPLE_TEXT);
content.setTextMatrix(Matrix.getRotateInstance(Math.toRadians(90), 300, 200));
content.showText(SAMPLE_TEXT);
content.endText();
}
try (document) {
TextDirection direction = AutoRotateDetection.detectTextDirections(document).get(0);
assertThat(direction.isConclusive()).isFalse();
}
}
@Test
void emptyPageIsNotConclusive() throws IOException {
try (PDDocument document = new PDDocument()) {
document.addPage(new PDPage(PDRectangle.LETTER));
TextDirection direction = AutoRotateDetection.detectTextDirections(document).get(0);
assertThat(direction.glyphCount()).isZero();
assertThat(direction.isConclusive()).isFalse();
}
}
@Test
void shortTextIsNotConclusive() throws IOException {
PDDocument document = new PDDocument();
PDPage page = new PDPage(PDRectangle.LETTER);
document.addPage(page);
try (PDPageContentStream content = new PDPageContentStream(document, page)) {
content.beginText();
content.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
content.setTextMatrix(Matrix.getTranslateInstance(100, 400));
content.showText("Short");
content.endText();
}
try (document) {
TextDirection direction = AutoRotateDetection.detectTextDirections(document).get(0);
assertThat(direction.isConclusive()).isFalse();
}
}
@Test
void unanimousShortTextIsConclusive() throws IOException {
// Between MIN_GLYPHS_UNANIMOUS (8) and MIN_GLYPHS (30): trusted only because every
// glyph agrees on direction, the sparse-page path (e.g. a lone header or URL line).
PDDocument document = new PDDocument();
PDPage page = new PDPage(PDRectangle.LETTER);
document.addPage(page);
try (PDPageContentStream content = new PDPageContentStream(document, page)) {
content.beginText();
content.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
content.setTextMatrix(Matrix.getRotateInstance(Math.toRadians(90), 300, 200));
content.showText("york.gov.uk/pay");
content.endText();
}
try (document) {
TextDirection direction = AutoRotateDetection.detectTextDirections(document).get(0);
assertThat(direction.glyphCount())
.isBetween(
AutoRotateDetection.MIN_GLYPHS_UNANIMOUS,
AutoRotateDetection.MIN_GLYPHS - 1);
assertThat(direction.dominance()).isEqualTo(1.0);
assertThat(direction.isConclusive()).isTrue();
assertThat(direction.dominantDirection()).isEqualTo(90);
}
}
@Test
void bucketsGlyphsPerPageInOneWalk() throws IOException {
// Each page carries text at a different angle; the single-pass walk must attribute
// glyphs to the right page rather than pooling them.
int[] angles = {0, 90, 180, 270};
try (PDDocument document = new PDDocument()) {
for (int angle : angles) {
PDPage page = new PDPage(PDRectangle.LETTER);
document.addPage(page);
try (PDPageContentStream content = new PDPageContentStream(document, page)) {
content.beginText();
content.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
content.setTextMatrix(
Matrix.getRotateInstance(Math.toRadians(angle), 300, 400));
content.showText(SAMPLE_TEXT);
content.endText();
}
}
List<TextDirection> directions = AutoRotateDetection.detectTextDirections(document);
assertThat(directions).hasSize(angles.length);
for (int i = 0; i < angles.length; i++) {
assertThat(directions.get(i).isConclusive()).as("page %d", i + 1).isTrue();
assertThat(directions.get(i).dominantDirection())
.as("page %d direction", i + 1)
.isEqualTo(angles[i]);
}
}
}
@Test
void detectsBlankAndInkedRenders() {
BufferedImage blank = new BufferedImage(200, 200, BufferedImage.TYPE_BYTE_GRAY);
Graphics2D g = blank.createGraphics();
g.setColor(Color.WHITE);
g.fillRect(0, 0, 200, 200);
g.dispose();
assertThat(AutoRotateDetection.isBlankRender(blank)).isTrue();
BufferedImage speck = copyOf(blank);
Graphics2D specked = speck.createGraphics();
specked.setColor(Color.BLACK);
specked.fillRect(0, 0, 2, 2); // a dust speck must not count as content
specked.dispose();
assertThat(AutoRotateDetection.isBlankRender(speck)).isTrue();
BufferedImage inked = copyOf(blank);
Graphics2D inkedG = inked.createGraphics();
inkedG.setColor(Color.BLACK);
inkedG.fillRect(20, 20, 120, 60);
inkedG.dispose();
assertThat(AutoRotateDetection.isBlankRender(inked)).isFalse();
}
private static BufferedImage copyOf(BufferedImage source) {
BufferedImage copy =
new BufferedImage(source.getWidth(), source.getHeight(), source.getType());
Graphics2D g = copy.createGraphics();
g.drawImage(source, 0, 0, null);
g.dispose();
return copy;
}
@Test
void parsesTypicalOsdOutput() {
String output =
"""
Estimating resolution as 336
Page number: 0
Orientation in degrees: 180
Rotate: 180
Orientation confidence: 9.15
Script: Latin
Script confidence: 4.43
""";
Optional<OsdResult> result = AutoRotateDetection.parseOsd(output);
assertThat(result).isPresent();
assertThat(result.get().rotate()).isEqualTo(180);
assertThat(result.get().confidence()).isEqualTo(9.15);
}
@Test
void parseOsdRejectsIncompleteOutput() {
assertThat(AutoRotateDetection.parseOsd("Too few characters. Skipping this page"))
.isEmpty();
assertThat(AutoRotateDetection.parseOsd("Rotate: 90")).isEmpty();
assertThat(AutoRotateDetection.parseOsd(null)).isEmpty();
assertThat(AutoRotateDetection.parseOsd("")).isEmpty();
}
}
Binary file not shown.
Binary file not shown.
+3
View File
@@ -67,6 +67,9 @@ dependencies {
implementation "software.amazon.awssdk:s3:${awsSdkVersion}"
implementation "software.amazon.awssdk:url-connection-client:${awsSdkVersion}"
// Streaming AEAD (AES-GCM-HKDF segments) for storage encryption at rest. Apache-2.0.
implementation "com.google.crypto.tink:tink:${tinkVersion}"
// @DataJpaTest slice (Boot 4 ships test slices as separate starters, like webmvc-test at the
// root) so policy.source repositories can be exercised against embedded H2.
testImplementation 'org.springframework.boot:spring-boot-starter-data-jpa-test'
@@ -115,6 +115,7 @@ public class ExternalApiCallController {
@RequestParam(value = "headers", required = false) String headers,
@RequestParam(value = "includeContext", defaultValue = "false") boolean includeContext,
@RequestParam(value = "includeFile", defaultValue = "true") boolean includeFile,
@RequestParam(value = "maxRequestBytes", defaultValue = "0") long maxRequestBytes,
@RequestHeader(value = InternalApiClient.POLICY_NAME_HEADER, required = false)
String policyName,
@RequestHeader(value = AutomationRunContext.RUN_ID_HEADER, required = false)
@@ -138,6 +139,17 @@ public class ExternalApiCallController {
: fileInput.getContentType();
byte[] content = fileInput.getBytes();
// Some destinations cap uploads (Discord's varies with Nitro tier), so the operator
// sets the limit; we fail clearly here rather than on an opaque vendor rejection.
if (maxRequestBytes > 0 && content.length > maxRequestBytes) {
throw new IllegalArgumentException(
"The document is "
+ megabytes(content.length)
+ " MB, over the "
+ megabytes(maxRequestBytes)
+ " MB limit set for this step.");
}
ObjectNode context =
DocumentContext.build(fileInput, content, policyName, runId, objectMapper);
@@ -545,6 +557,11 @@ public class ExternalApiCallController {
: oneLine.substring(0, MAX_REPORT_BODY_CHARS) + "";
}
/** Bytes as MB to one decimal, for a size message an operator reads in the units they set. */
private static String megabytes(long bytes) {
return String.format(Locale.ROOT, "%.1f", bytes / (1024.0 * 1024.0));
}
private static String safeFileName(String originalFilename) {
String name = Filenames.toSimpleFileName(originalFilename);
return (name == null || name.isBlank()) ? "document" : name;
@@ -21,6 +21,8 @@ import tools.jackson.databind.node.StringNode;
*/
final class Placeholders {
private static final int MAX_TREE_DEPTH = 64;
private static final Pattern PLACEHOLDER = Pattern.compile("\\{\\{\\s*([\\w.]+)\\s*}}");
/** How a resolved value is escaped for the position it lands in. */
@@ -68,15 +70,24 @@ final class Placeholders {
* documents[0].data} as readily as a flat field - without a connector per vendor.
*/
static JsonNode resolveTree(JsonNode node, JsonNode context) {
return resolveTree(node, context, 0);
}
private static JsonNode resolveTree(JsonNode node, JsonNode context, int depth) {
// Deeply nested JSON is left untouched rather than recursed into, so a
// pathological template cannot overflow the stack.
if (depth > MAX_TREE_DEPTH) {
return node;
}
if (node instanceof ObjectNode object) {
for (String name : new java.util.ArrayList<>(object.propertyNames())) {
object.set(name, resolveTree(object.get(name), context));
object.set(name, resolveTree(object.get(name), context, depth + 1));
}
return object;
}
if (node instanceof ArrayNode array) {
for (int i = 0; i < array.size(); i++) {
array.set(i, resolveTree(array.get(i), context));
array.set(i, resolveTree(array.get(i), context, depth + 1));
}
return array;
}
@@ -355,8 +355,7 @@ public class PolicyController {
policy.name(),
owner,
policy.enabled(),
policy.trigger(),
policy.sourceIds(),
policy.inputs(),
policy.steps(),
policy.output(),
policy.outputIds(),
@@ -3,6 +3,7 @@ package stirling.software.proprietary.policy.engine;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -33,6 +34,7 @@ import stirling.software.proprietary.service.AiToolResponseHeaders;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.node.ObjectNode;
/**
* Runs an ordered chain of tool steps, feeding each step's output files into the next.
@@ -49,6 +51,10 @@ public class PolicyExecutor {
private static final String FILTER_OPERATION_PREFIX = "/api/v1/filter/filter-";
// Nested list parameters are walked recursively; cap the depth so a pathological
// pipeline cannot overflow the stack.
private static final int MAX_PARAMETER_DEPTH = 32;
private final InternalApiClient internalApiClient;
private final ToolMetadataService toolMetadataService;
private final TempFileManager tempFileManager;
@@ -89,6 +95,10 @@ public class PolicyExecutor {
// Last non-null report wins: the terminal step defines the output.
JsonNode lastReport = null;
String lastReportTool = null;
// Every step's report, keyed by 1-based position, so a later step can reference an earlier
// one's response via {{steps.N...}} - e.g. post the share link an upload step returned.
ObjectNode runContext = objectMapper.createObjectNode();
ObjectNode stepReports = runContext.putObject("steps");
for (int i = 0; i < steps.size(); i++) {
PipelineStep step = steps.get(i);
@@ -98,13 +108,17 @@ public class PolicyExecutor {
"Pipeline step " + (i + 1) + " has no operation");
}
listener.onStepStart(i + 1, steps.size(), operation);
// Fill in references to earlier steps' outputs before dispatch; document- and run-scope
// placeholders are left for the tool to resolve per document.
PipelineStep resolved = resolveStepReferences(step, runContext);
StepOutput stepResult =
executeStep(step, currentFiles, currentOrigins, supportingFiles);
executeStep(resolved, currentFiles, currentOrigins, supportingFiles);
currentFiles = stepResult.files();
currentOrigins = stepResult.origins();
if (stepResult.report() != null) {
lastReport = stepResult.report();
lastReportTool = operation;
stepReports.set(String.valueOf(i + 1), stepResult.report());
}
listener.onStepComplete(i + 1, steps.size(), operation);
}
@@ -163,6 +177,92 @@ public class PolicyExecutor {
return new StepOutput(files, origins, report);
}
/**
* Resolve {@code {{steps.N...}}} references in a step's string parameters against the reports
* earlier steps produced. Returns the step unchanged when it references nothing, so a pipeline
* that uses no cross-step values pays nothing and behaves exactly as before.
*/
private PipelineStep resolveStepReferences(PipelineStep step, JsonNode runContext) {
boolean any = step.parameters().values().stream().anyMatch(this::referencesStep);
if (!any) {
return step;
}
Map<String, Object> resolved = new LinkedHashMap<>();
step.parameters()
.forEach((key, value) -> resolved.put(key, resolveValue(value, runContext)));
return new PipelineStep(step.operation(), resolved, step.fileParameters());
}
private boolean referencesStep(Object value) {
return referencesStep(value, 0);
}
private boolean referencesStep(Object value, int depth) {
if (depth > MAX_PARAMETER_DEPTH) {
return false;
}
if (value instanceof String s) {
return StepOutputPlaceholders.references(s);
}
if (value instanceof List<?> list) {
return list.stream().anyMatch(item -> referencesStep(item, depth + 1));
}
return false;
}
private Object resolveValue(Object value, JsonNode runContext) {
return resolveValue(value, runContext, 0);
}
private Object resolveValue(Object value, JsonNode runContext, int depth) {
if (depth > MAX_PARAMETER_DEPTH) {
return value;
}
if (value instanceof String s) {
return resolveString(s, runContext);
}
if (value instanceof List<?> list) {
List<Object> out = new ArrayList<>(list.size());
for (Object item : list) {
out.add(resolveValue(item, runContext, depth + 1));
}
return out;
}
return value;
}
/**
* A JSON-shaped parameter (bodyTemplate, fields, headers) is resolved inside its parsed tree,
* so an earlier step's response can only ever become a value in it. String-level substitution
* would let a response like {@code x", "admin": true, "y": "} inject fields into the JSON the
* operator wrote; a plain-text parameter keeps the plain substitution.
*/
private String resolveString(String value, JsonNode runContext) {
if (!StepOutputPlaceholders.references(value)) {
return value;
}
JsonNode tree = parseJsonContainer(value);
if (tree == null) {
return StepOutputPlaceholders.resolve(value, runContext);
}
return objectMapper.writeValueAsString(
StepOutputPlaceholders.resolveTree(tree, runContext));
}
/** The value parsed as a JSON object or array, or null when it is anything else. */
private JsonNode parseJsonContainer(String value) {
String trimmed = value.trim();
if (trimmed.isEmpty() || (trimmed.charAt(0) != '{' && trimmed.charAt(0) != '[')) {
return null;
}
try {
JsonNode node = objectMapper.readTree(value);
return (node.isObject() || node.isArray()) ? node : null;
} catch (JacksonException e) {
return null;
}
}
/**
* Call an endpoint, returning result files and optional report. Response handling: JSON body is
* the report with no file; a file body returns the file plus any {@link
@@ -15,6 +15,7 @@ import stirling.software.proprietary.policy.input.ResolvedInput;
import stirling.software.proprietary.policy.ledger.ProcessedLedger;
import stirling.software.proprietary.policy.model.InputSpec;
import stirling.software.proprietary.policy.model.PipelineDefinition;
import stirling.software.proprietary.policy.model.PipelineInput;
import stirling.software.proprietary.policy.model.Policy;
import stirling.software.proprietary.policy.model.PolicyInputs;
import stirling.software.proprietary.policy.model.PolicyRun;
@@ -42,30 +43,46 @@ public class PolicyRunner {
private final SourceDocCounter docCounter;
private final ProcessedLedger processedLedger;
/** Full-listing sweep: resolve every source, then reconcile the ledger. */
/** Full-listing sweep over every input: resolve each source, then reconcile the ledger. */
public SweepOutcome run(Policy policy) {
return run(policy, SweepKind.FULL);
}
/**
* Trigger entry point. Pulls every referenced source; each yielded unit becomes its own run so
* one failure does not affect the others. No sources means one run with no input (generator
* pipeline). Missing or disabled sources are skipped so one broken reference does not stop the
* rest. Returns the ids of the runs it started plus what the sweep skipped, so a manual trigger
* can report which runs to follow or why nothing ran.
*/
/** Sweep every input of the policy at the given listing depth. */
public SweepOutcome run(Policy policy, SweepKind sweep) {
return run(policy, policy.inputs(), sweep);
}
/**
* Fire one input binding: a background trigger pulling its own source without touching the
* policy's other inputs. Never reconciles the ledger (it sees a single source, so pruning would
* wrongly forget the rest); a full-policy sweep handles that.
*/
public SweepOutcome runInput(Policy policy, PipelineInput input, SweepKind sweep) {
return run(policy, List.of(input), sweep);
}
/**
* Core sweep: pulls each of the given inputs' sources; each yielded unit becomes its own run so
* one failure does not affect the others. No inputs means one run with no input (generator
* pipeline). Missing or disabled sources are skipped so one broken reference does not stop the
* rest. Presence cleanup only runs when the sweep covered every input of the policy - a
* single-binding fire cannot reconcile the whole policy's ledger. Returns the ids of the runs
* it started plus what the sweep skipped, so a manual trigger can report which runs to follow
* or why nothing ran.
*/
public SweepOutcome run(Policy policy, List<PipelineInput> inputs, SweepKind sweep) {
long sweepStart = System.currentTimeMillis();
PolicySweep context = new PolicySweep(policy.id(), sweep, processedLedger);
List<String> runIds = new ArrayList<>();
List<String> sourceIds = policy.sourceIds();
if (sourceIds.isEmpty()) {
if (inputs.isEmpty()) {
// Generator pipeline: one run with no input. Still fall through to the cleanup
// below so rows recorded for its folder outputs are pruned like anything else,
// instead of accumulating until the policy is deleted.
runIds.add(startRun(policy, PolicyInputs.of(List.of()), unused -> {}));
}
for (String sourceId : sourceIds) {
for (PipelineInput input : inputs) {
String sourceId = input.sourceId();
Source source = sourceStore.get(sourceId).orElse(null);
if (source == null) {
// No veto: a deleted source's rows should age out via the cleanup below.
@@ -84,7 +101,8 @@ public class PolicyRunner {
}
runIds.addAll(pullAndRun(policy, sourceId, source.toInputSpec(), context));
}
if (context.cleanupAllowed()) {
boolean fullPolicy = inputs.size() == policy.inputs().size();
if (fullPolicy && context.cleanupAllowed()) {
processedLedger.markSeen(policy.id(), context.presentIdentities());
int removed = processedLedger.deleteUnseen(policy.id(), sweepStart);
if (removed > 0) {
@@ -9,6 +9,7 @@ import lombok.RequiredArgsConstructor;
import stirling.software.proprietary.policy.input.InputSource;
import stirling.software.proprietary.policy.model.InputSpec;
import stirling.software.proprietary.policy.model.OutputSpec;
import stirling.software.proprietary.policy.model.PipelineInput;
import stirling.software.proprietary.policy.model.PipelineStep;
import stirling.software.proprietary.policy.model.Policy;
import stirling.software.proprietary.policy.model.TriggerConfig;
@@ -18,10 +19,11 @@ import stirling.software.proprietary.policy.source.SourceStore;
import stirling.software.proprietary.policy.trigger.PolicyTrigger;
/**
* Validates a policy at save time by delegating each facet (trigger, sources, steps, output) to the
* bean that handles its type, so a misconfiguration fails fast rather than at run time. A null
* trigger is a manual-only policy and skips trigger validation. Each referenced {@code sourceId}
* must resolve to a persisted {@link Source} whose config its {@link InputSource} bean accepts.
* Validates a policy at save time by delegating each facet (inputs, their triggers, output) to the
* bean that handles its type, so a misconfiguration fails fast rather than at run time. Each
* input's {@code sourceId} must resolve to a persisted {@link Source} whose config its {@link
* InputSource} bean accepts; its optional trigger must be a known type compatible with that source.
* A null trigger is a manual-only input and skips trigger validation.
*/
@Service
@RequiredArgsConstructor
@@ -34,21 +36,31 @@ public class PolicyValidator {
private final SourceStore sourceStore;
/**
* @throws IllegalArgumentException if any facet's type is unknown, a referenced source does not
* exist, or any config is invalid
* @throws IllegalArgumentException if the policy has more than one input or output, any facet's
* type is unknown, a referenced source does not exist, a trigger is incompatible with its
* input's source, or any config is invalid
*/
public void validate(Policy policy) {
if (policy.trigger() != null) {
triggerFor(policy.trigger()).validate(policy);
// Deliberate product cap, not a model limit: the lists stay lists so multiple
// inputs/outputs can be supported later, but today a policy carries at most one of
// each (zero of either remains fine - run on demand / inline output).
if (policy.inputs().size() > 1) {
throw new IllegalArgumentException("a policy supports at most one input");
}
for (String sourceId : policy.sourceIds()) {
if (policy.outputIds().size() > 1) {
throw new IllegalArgumentException("a policy supports at most one output");
}
for (PipelineInput input : policy.inputs()) {
Source source =
sourceStore
.get(sourceId)
.get(input.sourceId())
.orElseThrow(
() ->
new IllegalArgumentException(
"unknown source: " + sourceId));
"unknown source: " + input.sourceId()));
if (input.trigger() != null) {
validateTrigger(policy, input, source);
}
InputSpec spec = source.toInputSpec();
inputSourceFor(spec).validate(spec);
}
@@ -73,6 +85,24 @@ public class PolicyValidator {
}
}
/**
* Check an input's trigger is a known type whose source constraints its source satisfies (e.g.
* folder-watch only on a folder source), then let the trigger validate its own options.
*/
private void validateTrigger(Policy policy, PipelineInput input, Source source) {
PolicyTrigger trigger = triggerFor(input.trigger());
if (!trigger.supportedSourceTypes().isEmpty()
&& !trigger.supportedSourceTypes().contains(source.type())) {
throw new IllegalArgumentException(
"trigger '"
+ trigger.type()
+ "' is not compatible with source type '"
+ source.type()
+ "'");
}
trigger.validate(policy, input);
}
/**
* Validate an output spec against its sink. Must be called on a request thread (caller's
* principal present) so an S3 output's connection is authorization-checked against the caller -
@@ -0,0 +1,111 @@
package stirling.software.proprietary.policy.engine;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.node.ArrayNode;
import tools.jackson.databind.node.ObjectNode;
import tools.jackson.databind.node.StringNode;
/**
* Substitutes {@code {{steps.N...}}} references in a step's parameters against the reports the
* earlier steps of the same run produced.
*
* <p>This is pipeline-scope resolution, and it lives here rather than in the tool because only the
* executor can see the steps before the current one. Document- and run-scope placeholders ({@code
* {{document.*}}}, {@code {{run.*}}}) are deliberately left untouched for the tool to resolve per
* document; only {@code steps.*} is touched here, so the two passes never collide and neither has
* to know the other's namespace.
*
* <p>Deliberately not a template language, matching the document-scope resolver: dotted lookup and
* nothing else. A reference that names a step or field with no value fails the run rather than
* sending an empty value, so a typo or a forward reference surfaces as an error.
*/
final class StepOutputPlaceholders {
private static final int MAX_TREE_DEPTH = 64;
// Only steps.* is matched; a document/run reference is left verbatim for the downstream tool.
private static final Pattern STEP_REF = Pattern.compile("\\{\\{\\s*(steps\\.[\\w.]+?)\\s*}}");
private StepOutputPlaceholders() {}
/** Whether the text references an earlier step at all, so callers can skip resolving. */
static boolean references(String text) {
return text != null && STEP_REF.matcher(text).find();
}
/**
* @param template text that may contain {@code {{steps...}}} references; null passes through
* @param context the run context whose {@code steps} object is keyed by 1-based step number
* @throws IllegalArgumentException if a reference names a step or field the context does not
* hold, or names a non-scalar, so it cannot be inlined into a string parameter
*/
static String resolve(String template, JsonNode context) {
if (template == null || template.isEmpty()) {
return template;
}
Matcher matcher = STEP_REF.matcher(template);
StringBuilder out = new StringBuilder();
while (matcher.find()) {
String path = matcher.group(1);
JsonNode value = lookup(context, path);
if (value == null || value.isMissingNode() || !value.isValueNode()) {
throw new IllegalArgumentException(
"step reference '{{"
+ path
+ "}}' resolved to nothing; an earlier step must have produced that"
+ " value (a later step cannot be referenced, and only scalar"
+ " values can be inlined)");
}
matcher.appendReplacement(out, Matcher.quoteReplacement(value.asString()));
}
matcher.appendTail(out);
return out.toString();
}
/**
* Resolve every string inside a parsed JSON tree, leaving structure and non-strings alone.
* Mirrors the document-scope resolver: a substituted value lands in a text node and is escaped
* on serialise, so a response can never inject fields into the JSON the operator wrote.
*/
static JsonNode resolveTree(JsonNode node, JsonNode context) {
return resolveTree(node, context, 0);
}
private static JsonNode resolveTree(JsonNode node, JsonNode context, int depth) {
// Deeply nested JSON is left untouched rather than recursed into, so a
// pathological template cannot overflow the stack.
if (depth > MAX_TREE_DEPTH) {
return node;
}
if (node instanceof ObjectNode object) {
for (String name : new java.util.ArrayList<>(object.propertyNames())) {
object.set(name, resolveTree(object.get(name), context, depth + 1));
}
return object;
}
if (node instanceof ArrayNode array) {
for (int i = 0; i < array.size(); i++) {
array.set(i, resolveTree(array.get(i), context, depth + 1));
}
return array;
}
if (node != null && node.isString()) {
return StringNode.valueOf(resolve(node.asString(), context));
}
return node;
}
private static JsonNode lookup(JsonNode context, String path) {
JsonNode node = context;
for (String segment : path.split("\\.")) {
if (node == null || !node.isObject()) {
return null;
}
node = node.get(segment);
}
return node;
}
}
@@ -0,0 +1,16 @@
package stirling.software.proprietary.policy.model;
/**
* One input of a policy: a reference to a persisted {@code Source} paired with the {@link
* TriggerConfig} that decides when <em>this</em> source is pulled. The trigger lives on the
* binding, not on the source (so one connection can feed many policies on different schedules) and
* not on the policy (so a folder input can be watched while an S3 input on the same policy polls).
* A {@code null} trigger means this input is pulled only when the policy is run on demand.
*/
public record PipelineInput(String sourceId, TriggerConfig trigger) {
/** An input with no automatic trigger: pulled only on a manual run. */
public static PipelineInput manual(String sourceId) {
return new PipelineInput(sourceId, null);
}
}
@@ -3,29 +3,31 @@ package stirling.software.proprietary.policy.model;
import java.util.List;
/**
* A stored automation: ordered tool steps, input sources, and output destinations.
* A stored automation: ordered tool steps, input bindings, and output destinations.
*
* <p>Always runnable on demand. An optional {@link TriggerConfig} fires it automatically; a {@code
* null} trigger means manual-only. Trigger decides when; {@code sourceIds} reference the persisted
* {@code Source} locations (resolved live at run time) files come from; a run pulls from every
* referenced source. {@code outputIds} reference the {@code Source} locations (resolved live) a
* run's files are delivered to - a run is delivered to every one; when empty the inline {@link
* #output} is used (results returned to the caller), the case for editor and one-off policies.
* <p>Always runnable on demand. Each {@link PipelineInput} references a persisted {@code Source}
* connection (resolved live at run time) and carries its own optional {@link TriggerConfig}: the
* trigger decides when that source is pulled, so one input can be watched while another polls, and
* a {@code null} trigger makes that input manual-only. An input with no trigger, or a policy with
* no triggered inputs, still runs when the policy is run on demand; a manual run pulls every input.
*
* <p>{@code outputIds} reference the {@code Source} locations (resolved live) a run's files are
* delivered to - a run is delivered to every one; when empty the inline {@link #output} is used
* (results returned to the caller), the case for editor and one-off policies.
*/
public record Policy(
String id,
String name,
String owner,
boolean enabled,
TriggerConfig trigger,
List<String> sourceIds,
List<PipelineInput> inputs,
List<PipelineStep> steps,
OutputSpec output,
List<String> outputIds,
Long teamId) {
public Policy {
sourceIds = sourceIds == null ? List.of() : List.copyOf(sourceIds);
inputs = inputs == null ? List.of() : List.copyOf(inputs);
steps = steps == null ? List.of() : steps;
output = output == null ? OutputSpec.inline() : output;
outputIds = outputIds == null ? List.of() : List.copyOf(outputIds);
@@ -41,12 +43,11 @@ public record Policy(
String name,
String owner,
boolean enabled,
TriggerConfig trigger,
List<String> sourceIds,
List<PipelineInput> inputs,
List<PipelineStep> steps,
OutputSpec output,
Long teamId) {
this(id, name, owner, enabled, trigger, sourceIds, steps, output, List.of(), teamId);
this(id, name, owner, enabled, inputs, steps, output, List.of(), teamId);
}
/**
@@ -58,35 +59,35 @@ public record Policy(
String name,
String owner,
boolean enabled,
TriggerConfig trigger,
List<String> sourceIds,
List<PipelineInput> inputs,
List<PipelineStep> steps,
OutputSpec output) {
this(id, name, owner, enabled, trigger, sourceIds, steps, output, List.of(), null);
this(id, name, owner, enabled, inputs, steps, output, List.of(), null);
}
/** A policy with no configured sources (a generator, or files supplied directly to a run). */
public Policy(
String id,
String name,
String owner,
boolean enabled,
TriggerConfig trigger,
List<PipelineStep> steps,
OutputSpec output) {
this(id, name, owner, enabled, trigger, List.of(), steps, output, List.of(), null);
/** The source ids this policy pulls from, in input order; a derived view for reads. */
public List<String> sourceIds() {
return inputs.stream().map(PipelineInput::sourceId).toList();
}
/** The distinct trigger types configured across this policy's inputs (manual inputs aside). */
public List<String> triggerTypes() {
return inputs.stream()
.map(PipelineInput::trigger)
.filter(trigger -> trigger != null)
.map(TriggerConfig::type)
.distinct()
.toList();
}
/** A copy with the inline output replaced (e.g. resolved for the engine, or migrated). */
public Policy withOutput(OutputSpec resolved) {
return new Policy(
id, name, owner, enabled, trigger, sourceIds, steps, resolved, outputIds, teamId);
return new Policy(id, name, owner, enabled, inputs, steps, resolved, outputIds, teamId);
}
/** A copy referencing the given saved output destinations. */
public Policy withOutputIds(List<String> newOutputIds) {
return new Policy(
id, name, owner, enabled, trigger, sourceIds, steps, output, newOutputIds, teamId);
return new Policy(id, name, owner, enabled, inputs, steps, output, newOutputIds, teamId);
}
/**
@@ -0,0 +1,31 @@
package stirling.software.proprietary.policy.model;
import java.util.List;
/**
* A policy paired with one of its {@link PipelineInput}s: the unit a background trigger fires. A
* policy with two triggered inputs yields two bindings, so each fires independently on its own
* trigger and pulls only its own source.
*/
public record PolicyBinding(Policy policy, PipelineInput input) {
/**
* The bindings across these policies whose input carries a trigger of the given type. Shared by
* the {@code PolicyStore} implementations so every backend derives a trigger's bindings the
* same way. Callers pass the policies a background trigger should consider (i.e. the enabled
* ones).
*/
public static List<PolicyBinding> matching(List<Policy> policies, String triggerType) {
return policies.stream()
.flatMap(
policy ->
policy.inputs().stream()
.filter(
input ->
input.trigger() != null
&& triggerType.equals(
input.trigger().type()))
.map(input -> new PolicyBinding(policy, input)))
.toList();
}
}
@@ -14,7 +14,6 @@ import stirling.software.proprietary.policy.config.PolicyAccessGuard;
import stirling.software.proprietary.policy.model.OutputSpec;
import stirling.software.proprietary.policy.model.PipelineStep;
import stirling.software.proprietary.policy.model.Policy;
import stirling.software.proprietary.policy.model.TriggerConfig;
import stirling.software.proprietary.policy.source.Source;
import stirling.software.proprietary.policy.source.SourceAccessGuard;
import stirling.software.proprietary.policy.source.SourceStore;
@@ -73,7 +72,7 @@ public class PolicyOverviewService {
policy.name(),
policy.enabled(),
policy.enabled() ? "active" : "paused",
triggerSummary(policy.trigger()),
triggerSummary(policy),
sources,
steps,
outputSummary(policy, sourceNames),
@@ -95,9 +94,13 @@ public class PolicyOverviewService {
return outputSummary(policy.output());
}
/** A null trigger is a manual-only policy; otherwise the trigger's type keys the summary. */
private static String triggerSummary(TriggerConfig trigger) {
return trigger == null ? "manual" : trigger.type();
/**
* Summarise a policy's triggers for the overview row: "manual" when no input is triggered,
* otherwise the distinct trigger types across its inputs (e.g. "folder-watch, schedule").
*/
private static String triggerSummary(Policy policy) {
List<String> types = policy.triggerTypes();
return types.isEmpty() ? "manual" : String.join(", ", types);
}
private static String outputSummary(OutputSpec output) {
@@ -87,7 +87,6 @@ public class DefaultClassificationPolicySeeder {
POLICY_NAME,
"system",
true,
null,
List.of(),
List.of(new PipelineStep(CLASSIFY_ENDPOINT, Map.of())),
new OutputSpec("inline", options),
@@ -9,6 +9,7 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import stirling.software.proprietary.policy.model.Policy;
import stirling.software.proprietary.policy.model.PolicyBinding;
/**
* In-memory {@link PolicyStore} for tests and any future no-database mode. {@link JpaPolicyStore}
@@ -32,8 +33,7 @@ public class InProcessPolicyStore implements PolicyStore {
policy.name(),
policy.owner(),
policy.enabled(),
policy.trigger(),
policy.sourceIds(),
policy.inputs(),
policy.steps(),
policy.output(),
policy.outputIds(),
@@ -77,12 +77,9 @@ public class InProcessPolicyStore implements PolicyStore {
}
@Override
public List<Policy> findByTriggerType(String triggerType) {
return policies.values().stream()
.filter(Policy::enabled)
.filter(policy -> policy.trigger() != null)
.filter(policy -> triggerType.equals(policy.trigger().type()))
.toList();
public List<PolicyBinding> findBindingsByTriggerType(String triggerType) {
List<Policy> enabled = policies.values().stream().filter(Policy::enabled).toList();
return PolicyBinding.matching(enabled, triggerType);
}
@Override
@@ -12,8 +12,12 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.proprietary.policy.model.Policy;
import stirling.software.proprietary.policy.model.PolicyBinding;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.node.ArrayNode;
import tools.jackson.databind.node.ObjectNode;
/**
* Durable {@link PolicyStore} backed by JPA; the runtime store. Policies are persisted as JSON via
@@ -40,8 +44,7 @@ public class JpaPolicyStore implements PolicyStore {
policy.name(),
policy.owner(),
policy.enabled(),
policy.trigger(),
policy.sourceIds(),
policy.inputs(),
policy.steps(),
policy.output(),
policy.outputIds(),
@@ -52,7 +55,6 @@ public class JpaPolicyStore implements PolicyStore {
entity.setName(stored.name());
entity.setOwner(stored.owner());
entity.setEnabled(stored.enabled());
entity.setTriggerType(stored.trigger() == null ? null : stored.trigger().type());
entity.setTeamId(stored.teamId());
// Preserve an existing policy's run-order position; append a new one to the end of its
// team's queue (max + 1), so setting up a policy adds it last by default.
@@ -119,11 +121,13 @@ public class JpaPolicyStore implements PolicyStore {
}
@Override
public List<Policy> findByTriggerType(String triggerType) {
return repository.findByTriggerTypeAndEnabledTrue(triggerType).stream()
.map(this::toPolicy)
.flatMap(Optional::stream)
.toList();
public List<PolicyBinding> findBindingsByTriggerType(String triggerType) {
List<Policy> enabled =
repository.findByEnabledTrue().stream()
.map(this::toPolicy)
.flatMap(Optional::stream)
.toList();
return PolicyBinding.matching(enabled, triggerType);
}
@Override
@@ -139,7 +143,8 @@ public class JpaPolicyStore implements PolicyStore {
// One unreadable row must never abort a bulk read or crash startup.
private Optional<Policy> toPolicy(PolicyEntity entity) {
try {
return Optional.of(objectMapper.readValue(entity.getPolicyJson(), Policy.class));
JsonNode node = upgradeLegacyShape(objectMapper.readTree(entity.getPolicyJson()));
return Optional.of(objectMapper.treeToValue(node, Policy.class));
} catch (Exception e) {
log.error(
"Skipping unreadable policy id={} name={}: stored JSON could not be parsed"
@@ -150,4 +155,35 @@ public class JpaPolicyStore implements PolicyStore {
return Optional.empty();
}
}
/**
* Migrate a policy JSON blob written before triggers moved onto inputs. The old shape carried a
* single policy-level {@code trigger} and a {@code sourceIds} list; pair each source with that
* trigger so an upgraded policy keeps firing. A trigger incompatible with a source
* (folder-watch on an S3 source) is simply inert at run time, matching the old behaviour where
* such a source was never watched. New-shape blobs (already carrying {@code inputs}) are
* returned untouched.
*/
private JsonNode upgradeLegacyShape(JsonNode root) {
if (!(root instanceof ObjectNode obj) || obj.has("inputs")) {
return root;
}
JsonNode trigger = obj.get("trigger");
JsonNode sourceIds = obj.get("sourceIds");
ArrayNode inputs = objectMapper.createArrayNode();
if (sourceIds != null && sourceIds.isArray()) {
for (JsonNode sourceId : sourceIds) {
ObjectNode input = objectMapper.createObjectNode();
input.set("sourceId", sourceId);
if (trigger != null && !trigger.isNull()) {
input.set("trigger", trigger);
}
inputs.add(input);
}
}
obj.set("inputs", inputs);
obj.remove("trigger");
obj.remove("sourceIds");
return obj;
}
}
@@ -17,10 +17,10 @@ import stirling.software.proprietary.integration.crypto.LegacyDecryptStringConve
/**
* JPA row for a {@link stirling.software.proprietary.policy.model.Policy}. The whole policy lives
* as JSON in {@code policyJson} (authoritative on read); the scalar columns are denormalized copies
* for querying, notably {@code triggerType} + {@code enabled} so background triggers can fetch
* their policies, and {@code teamId} so the caller's team can be loaded without scanning every
* team's rows. {@code owner} and {@code teamId} are plain values, not foreign keys, to stay
* decoupled from the security entities.
* for querying, notably {@code enabled} so background triggers can scan the active policies, and
* {@code teamId} so the caller's team can be loaded without scanning every team's rows. {@code
* owner} and {@code teamId} are plain values, not foreign keys, to stay decoupled from the security
* entities.
*/
@Entity
@Table(name = "policies")
@@ -44,9 +44,6 @@ public class PolicyEntity implements Serializable {
@Column(name = "enabled")
private boolean enabled;
@Column(name = "trigger_type")
private String triggerType;
@Column(name = "team_id")
private Long teamId;
@@ -13,8 +13,12 @@ import jakarta.persistence.LockModeType;
@Repository
public interface PolicyRepository extends JpaRepository<PolicyEntity, String> {
/** Enabled policies of a given trigger type, for background triggers to activate. */
List<PolicyEntity> findByTriggerTypeAndEnabledTrue(String triggerType);
/**
* Enabled policies, for background triggers to scan for inputs of their trigger type. Which
* inputs (and their trigger types) a policy carries lives in the JSON blob, so the type filter
* is applied after parsing rather than in SQL.
*/
List<PolicyEntity> findByEnabledTrue();
/**
* Policies belonging to a team, in run order (ascending {@code sortOrder}; a null order sorts
@@ -4,6 +4,7 @@ import java.util.List;
import java.util.Optional;
import stirling.software.proprietary.policy.model.Policy;
import stirling.software.proprietary.policy.model.PolicyBinding;
/** Stores {@link Policy} definitions. */
public interface PolicyStore {
@@ -18,8 +19,11 @@ public interface PolicyStore {
/** Policies owned by the given team, loaded scoped rather than fetched globally. */
List<Policy> findByTeam(Long teamId);
/** Enabled policies with the given trigger type, for background triggers. */
List<Policy> findByTriggerType(String triggerType);
/**
* Enabled inputs with the given trigger type, as {@code (policy, input)} bindings, so a
* background trigger fires each input independently and pulls only its own source.
*/
List<PolicyBinding> findBindingsByTriggerType(String triggerType);
/**
* Set the team's run order from {@code orderedIds} (position → sortOrder). Only policies that
@@ -31,7 +31,9 @@ import stirling.software.proprietary.policy.engine.PolicyRunner;
import stirling.software.proprietary.policy.engine.SweepKind;
import stirling.software.proprietary.policy.input.InputSource;
import stirling.software.proprietary.policy.model.InputSpec;
import stirling.software.proprietary.policy.model.PipelineInput;
import stirling.software.proprietary.policy.model.Policy;
import stirling.software.proprietary.policy.model.PolicyBinding;
import stirling.software.proprietary.policy.source.Source;
import stirling.software.proprietary.policy.source.SourceStore;
import stirling.software.proprietary.policy.store.PolicyStore;
@@ -85,10 +87,10 @@ public class FolderWatchTrigger implements PolicyTrigger {
}
@Override
public void validate(Policy policy) {
if (watchDirsOf(policy).isEmpty()) {
public void validate(Policy policy, PipelineInput input) {
if (watchDirsOf(input).isEmpty()) {
throw new IllegalArgumentException(
"folder-watch trigger requires at least one watchable (folder) input source");
"folder-watch trigger requires a watchable (folder) input source");
}
}
@@ -185,24 +187,30 @@ public class FolderWatchTrigger implements PolicyTrigger {
return changed;
}
/** Run every folder-watch policy that draws from one of the changed directories. */
/** Fire every folder-watch input that draws from one of the changed directories. */
void runForChangedDirs(Set<Path> changedDirs) {
if (changedDirs.isEmpty()) {
return;
}
for (Policy policy : policyStore.findByTriggerType(TYPE)) {
for (PolicyBinding binding : policyStore.findBindingsByTriggerType(TYPE)) {
List<Path> dirs;
try {
dirs = watchDirsOf(policy);
dirs = watchDirsOf(binding.input());
} catch (RuntimeException e) {
log.warn(
"Folder-watch policy {} is misconfigured: {}", policy.id(), e.getMessage());
"Folder-watch input {}/{} is misconfigured: {}",
binding.policy().id(),
binding.input().sourceId(),
e.getMessage());
continue;
}
if (dirs.stream().anyMatch(changedDirs::contains)) {
log.debug("Folder-watch policy {} ({}) saw activity", policy.id(), policy.name());
log.debug(
"Folder-watch input {}/{} saw activity",
binding.policy().id(),
binding.input().sourceId());
// Light: the periodic reconcile does the full sweep.
policyRunner.run(policy, SweepKind.LIGHT);
policyRunner.runInput(binding.policy(), binding.input(), SweepKind.LIGHT);
}
}
}
@@ -216,15 +224,16 @@ public class FolderWatchTrigger implements PolicyTrigger {
}
}
/** Reconcile safety net: run every folder-watch policy regardless of watch events. */
/** Reconcile safety net: run every folder-watch input regardless of watch events. */
void runAll() {
for (Policy policy : policyStore.findByTriggerType(TYPE)) {
for (PolicyBinding binding : policyStore.findBindingsByTriggerType(TYPE)) {
try {
policyRunner.run(policy);
policyRunner.runInput(binding.policy(), binding.input(), SweepKind.FULL);
} catch (RuntimeException e) {
log.warn(
"Folder-watch reconcile run failed for policy {}: {}",
policy.id(),
"Folder-watch reconcile run failed for input {}/{}: {}",
binding.policy().id(),
binding.input().sourceId(),
e.getMessage());
}
}
@@ -269,41 +278,43 @@ public class FolderWatchTrigger implements PolicyTrigger {
return Set.copyOf(keysByDir.keySet());
}
/** Every existing directory any current folder-watch policy wants watched. */
/** Every existing directory any current folder-watch input wants watched. */
private Set<Path> desiredDirs() {
Set<Path> dirs = new HashSet<>();
for (Policy policy : policyStore.findByTriggerType(TYPE)) {
for (PolicyBinding binding : policyStore.findBindingsByTriggerType(TYPE)) {
try {
for (Path dir : watchDirsOf(policy)) {
for (Path dir : watchDirsOf(binding.input())) {
if (Files.isDirectory(dir)) {
dirs.add(dir);
}
}
} catch (RuntimeException e) {
log.warn(
"Folder-watch policy {} is misconfigured: {}", policy.id(), e.getMessage());
"Folder-watch input {}/{} is misconfigured: {}",
binding.policy().id(),
binding.input().sourceId(),
e.getMessage());
}
}
return dirs;
}
// Absolute + normalised so registration keys and event-time matching compare regardless of how
// the path was configured.
private List<Path> watchDirsOf(Policy policy) {
// the path was configured. Empty for a non-folder or missing source (that input is never
// watched), so a folder-watch trigger paired with an S3 input is simply inert.
private List<Path> watchDirsOf(PipelineInput input) {
List<Path> dirs = new ArrayList<>();
for (String sourceId : policy.sourceIds()) {
Source source = sourceStore.get(sourceId).orElse(null);
if (source == null) {
continue;
}
InputSpec spec = source.toInputSpec();
InputSource inputSource = sourceFor(spec);
if (inputSource == null) {
continue;
}
for (Path dir : inputSource.watchTargets(spec)) {
dirs.add(dir.toAbsolutePath().normalize());
}
Source source = sourceStore.get(input.sourceId()).orElse(null);
if (source == null) {
return dirs;
}
InputSpec spec = source.toInputSpec();
InputSource inputSource = sourceFor(spec);
if (inputSource == null) {
return dirs;
}
for (Path dir : inputSource.watchTargets(spec)) {
dirs.add(dir.toAbsolutePath().normalize());
}
return dirs;
}
@@ -2,11 +2,13 @@ package stirling.software.proprietary.policy.trigger;
import java.util.Set;
import stirling.software.proprietary.policy.model.PipelineInput;
import stirling.software.proprietary.policy.model.Policy;
/**
* Decides <em>when</em> a policy runs. On firing it hands the policy to {@code PolicyRunner}; it
* never resolves sources itself. New trigger kinds are just new beans of this type.
* Decides <em>when</em> a policy input runs. On firing it hands the binding to {@code
* PolicyRunner}, which pulls only that input's source; it never resolves sources itself. New
* trigger kinds are just new beans of this type.
*/
public interface PolicyTrigger {
@@ -32,10 +34,11 @@ public interface PolicyTrigger {
}
/**
* Validate at save time so misconfiguration fails fast, not at fire time. Receives the whole
* {@link Policy} so triggers that depend on the policy's sources (folder-watch) can check that.
* Validate one input's use of this trigger at save time so misconfiguration fails fast, not at
* fire time. Receives the owning {@link Policy} and the specific {@link PipelineInput} so a
* trigger that depends on the input's source (folder-watch) can check it.
*/
default void validate(Policy policy) {}
default void validate(Policy policy, PipelineInput input) {}
default void start() {}
@@ -17,14 +17,18 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.proprietary.policy.engine.PolicyRunner;
import stirling.software.proprietary.policy.engine.SweepKind;
import stirling.software.proprietary.policy.model.PipelineInput;
import stirling.software.proprietary.policy.model.Policy;
import stirling.software.proprietary.policy.model.PolicyBinding;
import stirling.software.proprietary.policy.model.Schedule;
import stirling.software.proprietary.policy.store.PolicyStore;
import tools.jackson.databind.ObjectMapper;
/**
* Fires policies on a {@link Schedule}: a fixed-interval sweep runs each due "schedule" policy.
* Fires policy inputs on a {@link Schedule}: a fixed-interval sweep pulls each due "schedule"
* input, independently of the policy's other inputs.
*
* <p>Last-fire times are in memory, so this assumes a single node and resets on restart.
*/
@@ -40,17 +44,20 @@ public class ScheduleTrigger implements PolicyTrigger {
private final ObjectMapper objectMapper;
private final ApplicationProperties applicationProperties;
private final Map<String, Instant> lastFiredByPolicy = new ConcurrentHashMap<>();
private final Map<BindingKey, Instant> lastFiredByBinding = new ConcurrentHashMap<>();
private volatile ScheduledExecutorService scheduler;
/** Identifies a schedule binding: one input (by source) of one policy. */
private record BindingKey(String policyId, String sourceId) {}
@Override
public String type() {
return TYPE;
}
@Override
public void validate(Policy policy) {
ScheduleConfig.from(objectMapper, policy.trigger().options());
public void validate(Policy policy, PipelineInput input) {
ScheduleConfig.from(objectMapper, input.trigger().options());
}
@Override
@@ -83,19 +90,26 @@ public class ScheduleTrigger implements PolicyTrigger {
}
}
/** Fire every scheduled policy that is due as of {@code now}. Package-visible for testing. */
/** Fire every scheduled input that is due as of {@code now}. Package-visible for testing. */
void sweep(Instant now) {
for (Policy policy : policyStore.findByTriggerType(TYPE)) {
for (PolicyBinding binding : policyStore.findBindingsByTriggerType(TYPE)) {
Policy policy = binding.policy();
PipelineInput input = binding.input();
ScheduleConfig config;
try {
config = ScheduleConfig.from(objectMapper, policy.trigger().options());
config = ScheduleConfig.from(objectMapper, input.trigger().options());
} catch (IllegalArgumentException e) {
log.warn("Scheduled policy {} is misconfigured: {}", policy.id(), e.getMessage());
log.warn(
"Scheduled input {}/{} is misconfigured: {}",
policy.id(),
input.sourceId(),
e.getMessage());
continue;
}
// Baseline a newly-seen policy to now so it does not fire immediately.
Instant last = lastFiredByPolicy.computeIfAbsent(policy.id(), id -> now);
// Baseline a newly-seen binding to now so it does not fire immediately.
BindingKey key = new BindingKey(policy.id(), input.sourceId());
Instant last = lastFiredByBinding.computeIfAbsent(key, id -> now);
ZonedDateTime next = config.schedule().nextAfter(last.atZone(config.zone()));
if (next.toInstant().isAfter(now)) {
continue;
@@ -105,9 +119,13 @@ public class ScheduleTrigger implements PolicyTrigger {
next = later;
later = config.schedule().nextAfter(later);
}
lastFiredByPolicy.put(policy.id(), next.toInstant());
log.info("Scheduled policy {} ({}) is due", policy.id(), policy.name());
policyRunner.run(policy);
lastFiredByBinding.put(key, next.toInstant());
log.info(
"Scheduled input {}/{} ({}) is due",
policy.id(),
input.sourceId(),
policy.name());
policyRunner.runInput(policy, input, SweepKind.FULL);
}
}
@@ -13,7 +13,9 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.proprietary.policy.engine.PolicyRunner;
import stirling.software.proprietary.policy.engine.SweepKind;
import stirling.software.proprietary.policy.model.PipelineInput;
import stirling.software.proprietary.policy.model.Policy;
import stirling.software.proprietary.policy.model.PolicyBinding;
import stirling.software.proprietary.policy.source.Source;
import stirling.software.proprietary.policy.source.SourceStore;
import stirling.software.proprietary.policy.store.PolicyStore;
@@ -50,15 +52,14 @@ public class WebhookTrigger implements PolicyTrigger {
}
@Override
public void validate(Policy policy) {
boolean hasWebhookSource =
policy.sourceIds().stream()
.map(sourceStore::get)
.flatMap(java.util.Optional::stream)
.anyMatch(source -> WEBHOOK_SOURCE_TYPE.equals(source.type()));
if (!hasWebhookSource) {
throw new IllegalArgumentException(
"webhook trigger requires at least one webhook input source");
public void validate(Policy policy, PipelineInput input) {
boolean isWebhookSource =
sourceStore
.get(input.sourceId())
.filter(source -> WEBHOOK_SOURCE_TYPE.equals(source.type()))
.isPresent();
if (!isWebhookSource) {
throw new IllegalArgumentException("webhook trigger requires a webhook input source");
}
}
@@ -83,29 +84,38 @@ public class WebhookTrigger implements PolicyTrigger {
}
}
/** Fire every webhook input fed by this webhook, pulling only that input's source. */
public void fireForWebhook(String webhookId) {
for (Policy policy : policyStore.findByTriggerType(TYPE)) {
if (!referencesWebhook(policy, webhookId)) {
for (PolicyBinding binding : policyStore.findBindingsByTriggerType(TYPE)) {
if (!referencesWebhook(binding.input(), webhookId)) {
continue;
}
try {
log.debug("Webhook policy {} ({}) saw a delivery", policy.id(), policy.name());
policyRunner.run(policy, SweepKind.LIGHT);
log.debug(
"Webhook input {}/{} saw a delivery",
binding.policy().id(),
binding.input().sourceId());
policyRunner.runInput(binding.policy(), binding.input(), SweepKind.LIGHT);
} catch (RuntimeException e) {
log.warn("Webhook run failed for policy {}: {}", policy.id(), e.getMessage());
log.warn(
"Webhook run failed for input {}/{}: {}",
binding.policy().id(),
binding.input().sourceId(),
e.getMessage());
}
}
}
private void safeReconcile() {
try {
for (Policy policy : policyStore.findByTriggerType(TYPE)) {
for (PolicyBinding binding : policyStore.findBindingsByTriggerType(TYPE)) {
try {
policyRunner.run(policy);
policyRunner.runInput(binding.policy(), binding.input(), SweepKind.FULL);
} catch (RuntimeException e) {
log.warn(
"Webhook reconcile run failed for policy {}: {}",
policy.id(),
"Webhook reconcile run failed for input {}/{}: {}",
binding.policy().id(),
binding.input().sourceId(),
e.getMessage());
}
}
@@ -114,17 +124,13 @@ public class WebhookTrigger implements PolicyTrigger {
}
}
private boolean referencesWebhook(Policy policy, String webhookId) {
for (String sourceId : policy.sourceIds()) {
Source source = sourceStore.get(sourceId).orElse(null);
if (source == null || !WEBHOOK_SOURCE_TYPE.equals(source.type())) {
continue;
}
Object configured = source.options().get(WebhookConfig.WEBHOOK_ID_OPTION);
if (configured != null && configured.toString().equals(webhookId)) {
return true;
}
/** Whether this input draws from the webhook source the delivery arrived on. */
private boolean referencesWebhook(PipelineInput input, String webhookId) {
Source source = sourceStore.get(input.sourceId()).orElse(null);
if (source == null || !WEBHOOK_SOURCE_TYPE.equals(source.type())) {
return false;
}
return false;
Object configured = source.options().get(WebhookConfig.WEBHOOK_ID_OPTION);
return configured != null && configured.toString().equals(webhookId);
}
}
@@ -6,20 +6,31 @@ import java.nio.file.Path;
import java.util.Locale;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionOperations;
import org.springframework.transaction.support.TransactionTemplate;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.configuration.InstallationPathConfig;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.util.TempFileManager;
import stirling.software.proprietary.cluster.s3.S3Clients;
import stirling.software.proprietary.security.configuration.ee.LicenseKeyChecker;
import stirling.software.proprietary.storage.crypto.EncryptingStorageProvider;
import stirling.software.proprietary.storage.crypto.FileEncryptionKeyService;
import stirling.software.proprietary.storage.crypto.FileEncryptionMasterKey;
import stirling.software.proprietary.storage.crypto.StorageEncryptionState;
import stirling.software.proprietary.storage.provider.DatabaseStorageProvider;
import stirling.software.proprietary.storage.provider.LocalStorageProvider;
import stirling.software.proprietary.storage.provider.S3StorageProvider;
import stirling.software.proprietary.storage.provider.StorageProvider;
import stirling.software.proprietary.storage.repository.FileEncryptionKeyRepository;
import stirling.software.proprietary.storage.repository.StoredFileBlobRepository;
@Configuration
@@ -29,10 +40,64 @@ public class StorageProviderConfig {
private final ApplicationProperties applicationProperties;
private final StoredFileBlobRepository storedFileBlobRepository;
private final FileEncryptionKeyRepository fileEncryptionKeyRepository;
private final LicenseKeyChecker licenseKeyChecker;
/**
* The encryption state behind the always-installed decorator. Key machinery is created eagerly
* when the write flag is on (licence-gated) or key rows already exist — so a wrong master key
* fails startup, not the first download — and lazily if encrypted content shows up later
* (config drift on one cluster node must fail loudly, never stream ciphertext). Turning the
* flag off or losing the licence only stops encrypting new writes; decryption stays available.
*/
@Bean
public StorageEncryptionState storageEncryptionState(
@Value("${stirling.security.fileEncryptionKey:}") String configuredFileEncryptionKey,
@Value("${cluster.enabled:false}") boolean clusterEnabled,
PlatformTransactionManager transactionManager) {
boolean writeEnabled = applicationProperties.getStorage().getEncryption().isEnabled();
if (writeEnabled) {
licenseKeyChecker.requireProOrEnterprise("storage.encryption");
}
// Key creation must commit independently of any caller transaction (see
// FileEncryptionKeyService#createActive).
TransactionTemplate requiresNew = new TransactionTemplate(transactionManager);
requiresNew.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
StorageEncryptionState state =
new StorageEncryptionState(
writeEnabled,
() ->
createKeyService(
configuredFileEncryptionKey, clusterEnabled, requiresNew),
fileEncryptionKeyRepository);
if (writeEnabled || fileEncryptionKeyRepository.count() > 0) {
state.initialiseEagerly();
log.info(
"Storage encryption at rest active (writes {})",
writeEnabled ? "encrypted" : "plaintext; decrypt-only mode");
}
return state;
}
private FileEncryptionKeyService createKeyService(
String configuredKey, boolean clusterEnabled, TransactionOperations keyCreationTx) {
FileEncryptionMasterKey masterKey =
new FileEncryptionMasterKey(configuredKey, clusterEnabled);
FileEncryptionKeyService keyService =
new FileEncryptionKeyService(fileEncryptionKeyRepository, masterKey, keyCreationTx);
// Wrong key must fail fast, not silently start a second key hierarchy.
keyService.verifyMasterKey();
return keyService;
}
@Bean(destroyMethod = "close")
public StorageProvider storageProvider() {
public StorageProvider storageProvider(
StorageEncryptionState encryptionState, Optional<TempFileManager> tempFileManager) {
return new EncryptingStorageProvider(
innerStorageProvider(), encryptionState, tempFileManager.orElse(null));
}
private StorageProvider innerStorageProvider() {
boolean storageEnabled = applicationProperties.getStorage().isEnabled();
String providerName =
Optional.ofNullable(applicationProperties.getStorage().getProvider())
@@ -0,0 +1,105 @@
package stirling.software.proprietary.storage.crypto;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.UUID;
/**
* On-disk format for storage-encrypted blobs. The object is self-describing so every {@code
* StorageProvider} backend stores identical bytes and legacy plaintext blobs are recognised by the
* absence of the magic:
*
* <pre>
* offset size field
* 0 8 magic "SPDFEAR1"
* 8 1 format version (1)
* 9 1 cipher suite (1 = AES-256-GCM-HKDF streaming, 1 MiB segments)
* 10 16 key id (file_encryption_keys row that wraps the DEK)
* 26 8 plaintext length
* 34 2 wrapped-DEK length (60 for suite 1)
* 36 60 wrapped DEK = IV(12) || AES-256-GCM(scope KEK, DEK) || tag(16)
* 96 .. payload: streaming-AEAD ciphertext
* </pre>
*
* <p>Bytes 033 are the associated data for both the DEK wrap and the payload AEAD, so neither a
* header transplanted onto another payload nor an altered key id / plaintext length authenticates.
*/
public final class EncryptedFileFormat {
public static final byte[] MAGIC = {'S', 'P', 'D', 'F', 'E', 'A', 'R', '1'};
public static final byte FORMAT_VERSION = 1;
public static final byte SUITE_AES_GCM_HKDF_1MIB = 1;
public static final int SEGMENT_SIZE_BYTES = 1 << 20;
public static final int DEK_LENGTH_BYTES = 32;
public static final int WRAPPED_DEK_LENGTH = 12 + DEK_LENGTH_BYTES + 16;
public static final int HEADER_LENGTH = 8 + 1 + 1 + 16 + 8 + 2 + WRAPPED_DEK_LENGTH;
private static final int AAD_LENGTH = 34;
private EncryptedFileFormat() {}
public record Header(
byte formatVersion,
byte cipherSuite,
UUID keyId,
long plaintextLength,
byte[] wrappedDek) {
public byte[] serialize() {
ByteBuffer buffer = ByteBuffer.allocate(HEADER_LENGTH);
buffer.put(MAGIC);
buffer.put(formatVersion);
buffer.put(cipherSuite);
buffer.putLong(keyId.getMostSignificantBits());
buffer.putLong(keyId.getLeastSignificantBits());
buffer.putLong(plaintextLength);
buffer.putShort((short) wrappedDek.length);
buffer.put(wrappedDek);
return buffer.array();
}
/** The header prefix (everything before the wrapped DEK) used as AEAD associated data. */
public byte[] associatedData() {
return Arrays.copyOfRange(serialize(), 0, AAD_LENGTH);
}
}
/**
* Parses a header from the first {@link #HEADER_LENGTH} bytes of a blob. Returns {@code null}
* when the bytes are not a storage-encrypted object (legacy plaintext passthrough).
*
* @throws StorageEncryptionException when the magic matches but the version/suite is unknown —
* checked so it flows through the callers' IOException error mapping instead of surfacing
* as a bare 500.
*/
public static Header parse(byte[] prefix) throws StorageEncryptionException {
if (prefix == null || prefix.length < HEADER_LENGTH) {
return null;
}
for (int i = 0; i < MAGIC.length; i++) {
if (prefix[i] != MAGIC[i]) {
return null;
}
}
ByteBuffer buffer = ByteBuffer.wrap(prefix, MAGIC.length, HEADER_LENGTH - MAGIC.length);
byte version = buffer.get();
byte suite = buffer.get();
UUID keyId = new UUID(buffer.getLong(), buffer.getLong());
long plaintextLength = buffer.getLong();
int wrappedDekLength = Short.toUnsignedInt(buffer.getShort());
if (version != FORMAT_VERSION
|| suite != SUITE_AES_GCM_HKDF_1MIB
|| plaintextLength < 0
|| wrappedDekLength != WRAPPED_DEK_LENGTH) {
throw new StorageEncryptionException(
"Unsupported storage-encryption header (version="
+ version
+ ", suite="
+ suite
+ "). This build cannot read the file.");
}
byte[] wrappedDek = new byte[wrappedDekLength];
buffer.get(wrappedDek);
return new Header(version, suite, keyId, plaintextLength, wrappedDek);
}
}
@@ -0,0 +1,470 @@
package stirling.software.proprietary.storage.crypto;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.SequenceInputStream;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.time.Duration;
import java.util.Arrays;
import java.util.Optional;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.springframework.core.io.AbstractResource;
import org.springframework.core.io.Resource;
import org.springframework.web.multipart.MultipartFile;
import com.google.crypto.tink.subtle.AesGcmHkdfStreaming;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.util.TempFileManager;
import stirling.software.proprietary.security.model.User;
import stirling.software.proprietary.storage.provider.StorageProvider;
import stirling.software.proprietary.storage.provider.StoredObject;
/**
* Envelope-encryption decorator over any {@link StorageProvider}: encrypts on {@code store} (when
* the write flag is on), decrypts on {@code load}, and passes legacy plaintext blobs through
* untouched (detected by the {@link EncryptedFileFormat} magic). Backends store opaque ciphertext
* and need no changes.
*
* <p>The decorator is installed unconditionally (see {@link StorageEncryptionState}), so a node
* whose config lags the cluster decrypts or fails loudly instead of streaming raw ciphertext.
* Decryption keeps working after the feature is switched off; only new writes revert to plaintext.
*
* <p>Presigned download URLs delegate to the backend only while no encrypted content can exist;
* otherwise they are suppressed, because an S3 presigned GET would hand ciphertext straight to the
* browser. Callers already fall back to app-streamed {@link #load} when no URL is offered.
*/
@Slf4j
public class EncryptingStorageProvider implements StorageProvider {
private static final SecureRandom RANDOM = new SecureRandom();
private final StorageProvider delegate;
private final StorageEncryptionState state;
private final TempFileManager tempFileManager;
public EncryptingStorageProvider(StorageProvider delegate, StorageEncryptionState state) {
this(delegate, state, null);
}
public EncryptingStorageProvider(
StorageProvider delegate,
StorageEncryptionState state,
TempFileManager tempFileManager) {
this.delegate = delegate;
this.state = state;
this.tempFileManager = tempFileManager;
}
/** Test convenience mirroring the pre-state constructor shape. */
public EncryptingStorageProvider(
StorageProvider delegate, FileEncryptionKeyService keys, boolean writeEnabled) {
this(delegate, StorageEncryptionState.of(writeEnabled, keys), null);
}
@Override
public StoredObject store(User owner, MultipartFile file) throws IOException {
if (!state.isWriteEnabled()) {
return delegate.store(owner, file);
}
FileEncryptionKeyService.ScopeKek kek = state.keyService().activeKekForOwner(owner);
byte[] dek = new byte[EncryptedFileFormat.DEK_LENGTH_BYTES];
RANDOM.nextBytes(dek);
// Spool ciphertext to a temp file: DatabaseStorageProvider needs getBytes() and
// S3StorageProvider needs an exact Content-Length, so the ciphertext size must be known
// before the delegate reads the upload. TempFileManager-registered so a crash mid-upload
// doesn't orphan the spool forever.
Path spool = createSpoolFile();
try {
EncryptedFileFormat.Header header = buildHeader(kek, dek, file.getSize());
byte[] aad = header.associatedData();
long plaintextBytes;
try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(spool))) {
out.write(header.serialize());
OutputStream encrypting = streamingAead(dek).newEncryptingStream(out, aad);
try (InputStream in = file.getInputStream()) {
plaintextBytes = in.transferTo(encrypting);
}
encrypting.close();
} catch (GeneralSecurityException e) {
throw new StorageEncryptionException("Failed to encrypt upload", e);
}
// The header (and AAD) already carry file.getSize(); a MultipartFile that mis-reports
// would otherwise surface as a Content-Length mismatch, i.e. a truncated or hanging
// download instead of an error.
if (plaintextBytes != file.getSize()) {
throw new StorageEncryptionException(
"Upload reported "
+ file.getSize()
+ " bytes but streamed "
+ plaintextBytes);
}
StoredObject stored = delegate.store(owner, new SpooledUpload(file, spool));
log.debug(
"Encrypted {} under key {} ({} plaintext bytes)",
stored.getStorageKey(),
kek.keyId(),
file.getSize());
return stored.toBuilder()
.sizeBytes(file.getSize())
.encryptionKeyId(kek.keyId().toString())
.build();
} finally {
Arrays.fill(dek, (byte) 0);
Files.deleteIfExists(spool);
}
}
private Path createSpoolFile() throws IOException {
if (tempFileManager != null) {
return tempFileManager.createTempFile(".enc").toPath();
}
return Files.createTempFile("stirling-enc-", ".bin");
}
@Override
public Resource load(String storageKey) throws IOException {
Resource raw = delegate.load(storageKey);
if (raw.isOpen()) {
return wrapOneShot(raw);
}
return wrapReopenable(raw);
}
@Override
public void delete(String storageKey) throws IOException {
delegate.delete(storageKey);
}
@Override
public void close() {
try {
delegate.close();
} catch (Exception e) {
log.warn("Error closing delegate storage provider", e);
}
}
@Override
public Optional<URI> signedDownloadUrl(String storageKey, Duration ttl) throws IOException {
if (state.suppressDirectDownloads()) {
return Optional.empty();
}
return delegate.signedDownloadUrl(storageKey, ttl);
}
@Override
public Optional<URI> signedDownloadUrl(
String storageKey, Duration ttl, boolean inline, String originalFilename)
throws IOException {
if (state.suppressDirectDownloads()) {
return Optional.empty();
}
return delegate.signedDownloadUrl(storageKey, ttl, inline, originalFilename);
}
// ---- store helpers -------------------------------------------------------------------
private EncryptedFileFormat.Header buildHeader(
FileEncryptionKeyService.ScopeKek kek, byte[] dek, long plaintextLength)
throws StorageEncryptionException {
// AAD covers the header prefix, so build a header with a placeholder wrap first to get
// the prefix bytes, then wrap the DEK bound to that prefix.
EncryptedFileFormat.Header prototype =
new EncryptedFileFormat.Header(
EncryptedFileFormat.FORMAT_VERSION,
EncryptedFileFormat.SUITE_AES_GCM_HKDF_1MIB,
kek.keyId(),
plaintextLength,
new byte[EncryptedFileFormat.WRAPPED_DEK_LENGTH]);
byte[] aad = prototype.associatedData();
byte[] wrappedDek = wrapDek(dek, kek.key(), aad);
return new EncryptedFileFormat.Header(
EncryptedFileFormat.FORMAT_VERSION,
EncryptedFileFormat.SUITE_AES_GCM_HKDF_1MIB,
kek.keyId(),
plaintextLength,
wrappedDek);
}
private static byte[] wrapDek(byte[] dek, byte[] kek, byte[] aad)
throws StorageEncryptionException {
try {
byte[] iv = new byte[12];
RANDOM.nextBytes(iv);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(
Cipher.ENCRYPT_MODE,
new SecretKeySpec(kek, "AES"),
new GCMParameterSpec(128, iv));
cipher.updateAAD(aad);
byte[] ct = cipher.doFinal(dek);
byte[] out = new byte[iv.length + ct.length];
System.arraycopy(iv, 0, out, 0, iv.length);
System.arraycopy(ct, 0, out, iv.length, ct.length);
return out;
} catch (GeneralSecurityException e) {
throw new StorageEncryptionException("Failed to wrap file key", e);
}
}
private byte[] unwrapDek(EncryptedFileFormat.Header header) throws IOException {
byte[] kek = state.keyService().kekForDecrypt(header.keyId());
try {
byte[] wrapped = header.wrappedDek();
byte[] iv = Arrays.copyOfRange(wrapped, 0, 12);
byte[] ct = Arrays.copyOfRange(wrapped, 12, wrapped.length);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(
Cipher.DECRYPT_MODE,
new SecretKeySpec(kek, "AES"),
new GCMParameterSpec(128, iv));
cipher.updateAAD(header.associatedData());
return cipher.doFinal(ct);
} catch (GeneralSecurityException e) {
throw new StorageEncryptionException(
"Failed to unwrap file key for key " + header.keyId() + " — tampered header?",
e);
}
}
/**
* Uses Tink's {@code subtle} API directly rather than the keyset/StreamingAead registry:
* keysets would store DEKs in Tink's own serialisation and key-management model, while this
* format keeps the (already-wrapped) DEK in our header. {@code subtle} is outside Tink's
* stability guarantee — the pinned version plus the round-trip tests are what make upgrades
* safe; re-run them on any Tink bump.
*/
private static AesGcmHkdfStreaming streamingAead(byte[] dek) throws GeneralSecurityException {
return new AesGcmHkdfStreaming(
dek,
"HMACSHA256",
EncryptedFileFormat.DEK_LENGTH_BYTES,
EncryptedFileFormat.SEGMENT_SIZE_BYTES,
0);
}
// ---- load helpers --------------------------------------------------------------------
/** Re-openable delegate (local file, DB byte array): sniff via a throwaway stream. */
private Resource wrapReopenable(Resource raw) throws IOException {
byte[] prefix;
try (InputStream in = raw.getInputStream()) {
prefix = in.readNBytes(EncryptedFileFormat.HEADER_LENGTH);
}
EncryptedFileFormat.Header header = EncryptedFileFormat.parse(prefix);
if (header == null) {
return raw;
}
byte[] dek = unwrapDek(header);
return new ReopenableDecryptedResource(raw, header, dek);
}
/**
* One-shot delegate (S3 stream): the sniffed prefix must be replayed or decrypted inline. The
* stream is a live HTTP connection, so every failure path (unknown header version, revoked key,
* tampered wrap) must close it — leaking here would starve the S3 connection pool precisely
* when the kill switch is being exercised.
*/
private Resource wrapOneShot(Resource raw) throws IOException {
InputStream in = raw.getInputStream();
try {
byte[] prefix = in.readNBytes(EncryptedFileFormat.HEADER_LENGTH);
EncryptedFileFormat.Header header = EncryptedFileFormat.parse(prefix);
if (header == null) {
long length;
try {
length = raw.contentLength();
} catch (IOException | RuntimeException e) {
// Stock InputStreamResource refuses contentLength() once the stream is
// partially read; S3's resource reports it from the response header instead.
length = -1;
}
return new OneShotResource(
new SequenceInputStream(new ByteArrayInputStream(prefix), in),
length,
raw.getDescription());
}
byte[] dek = unwrapDek(header);
InputStream decrypting;
try {
decrypting = streamingAead(dek).newDecryptingStream(in, header.associatedData());
} catch (GeneralSecurityException e) {
throw new StorageEncryptionException("Failed to open decrypting stream", e);
}
return new OneShotResource(decrypting, header.plaintextLength(), raw.getDescription());
} catch (IOException | RuntimeException e) {
try {
in.close();
} catch (IOException closeFailure) {
e.addSuppressed(closeFailure);
}
throw e;
}
}
/**
* Fresh decrypting stream per read; supports repeated reads and range-skip consumers. Note a
* range request still decrypts from byte 0 and discards up to the offset — inherent to
* streaming AEAD without a seekable-channel implementation.
*/
private static final class ReopenableDecryptedResource extends AbstractResource {
private final Resource ciphertext;
private final EncryptedFileFormat.Header header;
private final byte[] dek;
private ReopenableDecryptedResource(
Resource ciphertext, EncryptedFileFormat.Header header, byte[] dek) {
this.ciphertext = ciphertext;
this.header = header;
this.dek = dek;
}
@Override
public InputStream getInputStream() throws IOException {
InputStream in = ciphertext.getInputStream();
try {
in.skipNBytes(EncryptedFileFormat.HEADER_LENGTH);
return streamingAead(dek).newDecryptingStream(in, header.associatedData());
} catch (GeneralSecurityException | IOException e) {
in.close();
throw e instanceof IOException io
? io
: new StorageEncryptionException("Failed to open decrypting stream", e);
}
}
@Override
public long contentLength() {
return header.plaintextLength();
}
@Override
public boolean exists() {
return ciphertext.exists();
}
@Override
public String getFilename() {
return ciphertext.getFilename();
}
@Override
public String getDescription() {
return "decrypted " + ciphertext.getDescription();
}
}
/** Single-use resource over an already-open stream (mirrors InputStreamResource semantics). */
private static final class OneShotResource extends AbstractResource {
private final InputStream stream;
private final long contentLength;
private final String description;
private boolean consumed;
private OneShotResource(InputStream stream, long contentLength, String description) {
this.stream = stream;
this.contentLength = contentLength;
this.description = description;
}
@Override
public synchronized InputStream getInputStream() {
if (consumed) {
throw new IllegalStateException(
"InputStream has already been read - do not use OneShotResource twice");
}
consumed = true;
return stream;
}
@Override
public long contentLength() {
return contentLength;
}
@Override
public boolean isOpen() {
return true;
}
@Override
public boolean exists() {
return true;
}
@Override
public String getDescription() {
return "decrypted " + description;
}
}
/** Presents the spooled ciphertext file as the upload the delegate should persist. */
private static final class SpooledUpload implements MultipartFile {
private final MultipartFile original;
private final Path spool;
private SpooledUpload(MultipartFile original, Path spool) {
this.original = original;
this.spool = spool;
}
@Override
public String getName() {
return original.getName();
}
@Override
public String getOriginalFilename() {
return original.getOriginalFilename();
}
@Override
public String getContentType() {
return original.getContentType();
}
@Override
public boolean isEmpty() {
return getSize() == 0;
}
@Override
public long getSize() {
try {
return Files.size(spool);
} catch (IOException e) {
throw new IllegalStateException("Spooled ciphertext unavailable", e);
}
}
@Override
public byte[] getBytes() throws IOException {
return Files.readAllBytes(spool);
}
@Override
public InputStream getInputStream() throws IOException {
return Files.newInputStream(spool);
}
@Override
public void transferTo(File dest) throws IOException {
Files.copy(spool, dest.toPath());
}
}
}
@@ -0,0 +1,214 @@
package stirling.software.proprietary.storage.crypto;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.time.Duration;
import java.util.Base64;
import java.util.UUID;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.transaction.support.TransactionOperations;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j;
import stirling.software.proprietary.model.Team;
import stirling.software.proprietary.security.model.User;
import stirling.software.proprietary.storage.model.FileEncryptionKey;
import stirling.software.proprietary.storage.repository.FileEncryptionKeyRepository;
/**
* Resolves and unwraps scope-level KEKs for storage encryption. Per-team scoping by default (files
* keep decrypting after team changes because each blob header pins its exact key id); GLOBAL is the
* fallback for owners without a team; SOURCE is reserved for pipeline encryption (P2).
*
* <p>Unwrapped KEKs are cached briefly so the kill switch (DISABLED status) propagates across
* cluster nodes within {@link #CACHE_TTL} without a per-read DB round-trip. Note the TTL bounds the
* write side too: a key disabled on another node can wrap new blobs for up to the TTL there.
*/
@Slf4j
public class FileEncryptionKeyService {
private static final Duration CACHE_TTL = Duration.ofSeconds(60);
private static final SecureRandom RANDOM = new SecureRandom();
private final FileEncryptionKeyRepository repository;
private final FileEncryptionMasterKey masterKey;
/**
* Runs key-row creation in its own committed transaction (REQUIRES_NEW in production). Callers
* like WorkflowSessionService are {@code @Transactional}, and with an assigned-UUID id the
* INSERT would otherwise defer to the outer commit — the duplicate-key exception would surface
* far from {@link #createActive}'s recovery catch, and the outer transaction would already be
* rollback-only.
*/
private final TransactionOperations keyCreationTx;
private final Cache<UUID, byte[]> unwrapCache =
Caffeine.newBuilder().expireAfterWrite(CACHE_TTL).maximumSize(10_000).build();
private final Cache<String, UUID> activeScopeCache =
Caffeine.newBuilder().expireAfterWrite(CACHE_TTL).maximumSize(10_000).build();
public FileEncryptionKeyService(
FileEncryptionKeyRepository repository, FileEncryptionMasterKey masterKey) {
this(repository, masterKey, TransactionOperations.withoutTransaction());
}
public FileEncryptionKeyService(
FileEncryptionKeyRepository repository,
FileEncryptionMasterKey masterKey,
TransactionOperations keyCreationTx) {
this.repository = repository;
this.masterKey = masterKey;
this.keyCreationTx = keyCreationTx;
}
public record ScopeKek(UUID keyId, byte[] key) {}
/** The ACTIVE KEK for the owner's scope, created on first use. */
public ScopeKek activeKekForOwner(User owner) throws StorageEncryptionException {
FileEncryptionKey.ScopeType scopeType = FileEncryptionKey.ScopeType.GLOBAL;
long scopeId = 0;
Team team = owner != null ? owner.getTeam() : null;
if (team != null && team.getId() != null) {
scopeType = FileEncryptionKey.ScopeType.TEAM;
scopeId = team.getId();
}
UUID cachedId = activeScopeCache.getIfPresent(scopeType + ":" + scopeId);
if (cachedId != null) {
byte[] cachedKey = unwrapCache.getIfPresent(cachedId);
if (cachedKey != null) {
return new ScopeKek(cachedId, cachedKey);
}
}
FileEncryptionKey row = findOrCreateActive(scopeType, scopeId);
byte[] kek = unwrapRow(row);
activeScopeCache.put(scopeType + ":" + scopeId, row.getKeyId());
return new ScopeKek(row.getKeyId(), kek);
}
/** Unwraps the KEK for decrypting an existing blob. Fails closed on DISABLED or missing. */
public byte[] kekForDecrypt(UUID keyId) throws StorageEncryptionException {
byte[] cached = unwrapCache.getIfPresent(keyId);
if (cached != null) {
return cached;
}
FileEncryptionKey row =
repository
.findById(keyId)
.orElseThrow(
() ->
new StorageEncryptionException(
"No encryption key "
+ keyId
+ " — the key registry does not match the"
+ " stored data (restored from an older"
+ " database backup?)"));
if (row.getStatus() == FileEncryptionKey.Status.DISABLED) {
throw new StorageKeyRevokedException(
"Encryption key " + keyId + " is disabled; access to this content is revoked");
}
return unwrapRow(row);
}
/**
* Startup self-check: proves the resolved master key can unwrap an existing row, so a wrong key
* fails fast instead of silently writing new files under a second key hierarchy.
*/
public void verifyMasterKey() {
repository
.findFirstByStatus(FileEncryptionKey.Status.ACTIVE)
.or(() -> repository.findFirstByStatus(FileEncryptionKey.Status.RETIRED))
.ifPresent(
row -> {
try {
unwrapRow(row);
} catch (StorageEncryptionException e) {
throw new IllegalStateException(
"The configured file encryption key (fingerprint "
+ masterKey.fingerprint()
+ ") cannot unwrap existing key "
+ row.getKeyId()
+ ". Refusing to start with a mismatched key —"
+ " restore the original"
+ " STIRLING_FILE_ENCRYPTION_KEY /"
+ " file-encryption.key.",
e);
}
});
}
private FileEncryptionKey findOrCreateActive(
FileEncryptionKey.ScopeType scopeType, long scopeId) throws StorageEncryptionException {
return repository
.findFirstByScopeTypeAndScopeIdAndStatus(
scopeType, scopeId, FileEncryptionKey.Status.ACTIVE)
.orElseGet(() -> createActive(scopeType, scopeId));
}
// Package-private so the @DataJpaTest can drive the duplicate-insert recovery
// deterministically.
FileEncryptionKey createActive(FileEncryptionKey.ScopeType scopeType, long scopeId) {
byte[] kek = new byte[EncryptedFileFormat.DEK_LENGTH_BYTES];
RANDOM.nextBytes(kek);
FileEncryptionKey row = new FileEncryptionKey();
row.setKeyId(UUID.randomUUID());
row.setScopeType(scopeType);
row.setScopeId(scopeId);
int version =
repository
.findFirstByScopeTypeAndScopeIdOrderByKeyVersionDesc(
scopeType, scopeId)
.map(FileEncryptionKey::getKeyVersion)
.orElse(0)
+ 1;
row.setKeyVersion(version);
row.setWrappedKey(
Base64.getEncoder().encodeToString(masterKey.wrap(kek, aadFor(row.getKeyId()))));
row.setMasterKeyVersion(FileEncryptionMasterKey.CURRENT_VERSION);
row.setStatus(FileEncryptionKey.Status.ACTIVE);
try {
// saveAndFlush inside a fresh transaction so a unique-constraint violation surfaces
// right here (not at some outer commit) and the caller's transaction stays healthy.
FileEncryptionKey saved = keyCreationTx.execute(status -> repository.saveAndFlush(row));
log.info(
"Created storage encryption key {} for {}:{}",
saved.getKeyId(),
scopeType,
scopeId);
unwrapCache.put(saved.getKeyId(), kek);
return saved;
} catch (DataIntegrityViolationException raced) {
// Another node created the scope key concurrently; use theirs.
return repository
.findFirstByScopeTypeAndScopeIdAndStatus(
scopeType, scopeId, FileEncryptionKey.Status.ACTIVE)
.orElseThrow(() -> raced);
}
}
private byte[] unwrapRow(FileEncryptionKey row) throws StorageEncryptionException {
try {
byte[] kek =
masterKey.unwrap(
Base64.getDecoder().decode(row.getWrappedKey()),
aadFor(row.getKeyId()));
unwrapCache.put(row.getKeyId(), kek);
return kek;
} catch (GeneralSecurityException e) {
throw new StorageEncryptionException(
"Failed to unwrap encryption key "
+ row.getKeyId()
+ " — master key mismatch or corrupted key row",
e);
}
}
// Binds each wrapped KEK to its row identity so ciphertexts can't be swapped between rows.
private static byte[] aadFor(UUID keyId) {
return keyId.toString().getBytes(StandardCharsets.US_ASCII);
}
}
@@ -0,0 +1,172 @@
package stirling.software.proprietary.storage.crypto;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
import java.util.EnumSet;
import java.util.HexFormat;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.configuration.InstallationPathConfig;
/**
* Master key-encryption key for storage encryption at rest. Deliberately a separate key from {@code
* credential-encryption.key}: file data and integration secrets have different blast radii and
* rotate independently.
*
* <p>Resolution order mirrors {@code CredentialEncryption}: {@code
* stirling.security.fileEncryptionKey} property, {@code STIRLING_FILE_ENCRYPTION_KEY} env var, then
* an auto-generated owner-only {@code file-encryption.key} in the config dir. Cluster mode requires
* an explicitly shared key.
*/
@Slf4j
public class FileEncryptionMasterKey {
private static final String ALGORITHM = "AES";
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
private static final int GCM_TAG_BITS = 128;
private static final int IV_BYTES = 12;
private static final String KEY_FILE = "file-encryption.key";
private static final SecureRandom RANDOM = new SecureRandom();
/** Bumped when master rotation ships (P2); recorded on every wrapped KEK row. */
public static final int CURRENT_VERSION = 1;
private final SecretKey key;
public FileEncryptionMasterKey(String configuredKey, boolean clusterEnabled) {
this.key = resolveKey(configuredKey, clusterEnabled);
log.info(
"Storage encryption master key initialised (AES-256-GCM, fingerprint {})",
fingerprint());
}
private static SecretKey resolveKey(String configuredKey, boolean clusterEnabled) {
String configured = configuredKey;
String source = "stirling.security.fileEncryptionKey";
if (configured == null || configured.isBlank()) {
configured = System.getenv("STIRLING_FILE_ENCRYPTION_KEY");
source = "STIRLING_FILE_ENCRYPTION_KEY";
}
if (configured != null && !configured.isBlank()) {
return decodeKey(configured, source);
}
if (clusterEnabled) {
throw new IllegalStateException(
"cluster.enabled=true requires a shared file encryption key. Set"
+ " STIRLING_FILE_ENCRYPTION_KEY (or"
+ " stirling.security.fileEncryptionKey) to the same value on every"
+ " node.");
}
return loadOrCreateKeyFile();
}
/**
* Decodes and validates key material: must be valid base64 for exactly 32 bytes. Without the
* length check a short key would silently downgrade to AES-128/192 while the startup log claims
* AES-256.
*/
private static SecretKey decodeKey(String base64, String source) {
byte[] bytes;
try {
bytes = Base64.getDecoder().decode(base64.trim());
} catch (IllegalArgumentException e) {
throw new IllegalStateException(source + " is not valid base64", e);
}
if (bytes.length != 32) {
throw new IllegalStateException(
source
+ " must decode to exactly 32 bytes (a 256-bit AES key), got "
+ bytes.length
+ " bytes. Generate one with: openssl rand -base64 32");
}
return new SecretKeySpec(bytes, ALGORITHM);
}
private static SecretKey loadOrCreateKeyFile() {
Path path = Path.of(InstallationPathConfig.getConfigPath(), KEY_FILE);
try {
if (Files.exists(path)) {
return decodeKey(Files.readString(path), path.toString());
}
KeyGenerator generator = KeyGenerator.getInstance(ALGORITHM);
generator.init(256);
SecretKey generated = generator.generateKey();
Files.createDirectories(path.getParent());
writeOwnerOnly(path, Base64.getEncoder().encodeToString(generated.getEncoded()));
log.warn(
"Generated a new file encryption key at {}. Back this file up: losing it makes"
+ " every encrypted stored file unrecoverable.",
path);
return generated;
} catch (Exception e) {
throw new IllegalStateException("Unable to initialise file encryption key", e);
}
}
private static void writeOwnerOnly(Path path, String content) throws IOException {
EnumSet<PosixFilePermission> ownerOnly =
EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE);
try {
Files.createFile(path, PosixFilePermissions.asFileAttribute(ownerOnly));
} catch (UnsupportedOperationException e) {
// Non-POSIX filesystem (Windows): the config-dir ACL is the protection.
Files.createFile(path);
}
Files.writeString(path, content);
try {
Files.setPosixFilePermissions(path, ownerOnly);
} catch (UnsupportedOperationException ignored) {
}
}
public byte[] wrap(byte[] kek, byte[] associatedData) {
try {
byte[] iv = new byte[IV_BYTES];
RANDOM.nextBytes(iv);
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_BITS, iv));
cipher.updateAAD(associatedData);
byte[] ciphertext = cipher.doFinal(kek);
byte[] combined = new byte[iv.length + ciphertext.length];
System.arraycopy(iv, 0, combined, 0, iv.length);
System.arraycopy(ciphertext, 0, combined, iv.length, ciphertext.length);
return combined;
} catch (GeneralSecurityException e) {
throw new IllegalStateException("Failed to wrap scope key", e);
}
}
public byte[] unwrap(byte[] wrapped, byte[] associatedData) throws GeneralSecurityException {
byte[] iv = Arrays.copyOfRange(wrapped, 0, IV_BYTES);
byte[] ciphertext = Arrays.copyOfRange(wrapped, IV_BYTES, wrapped.length);
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_BITS, iv));
cipher.updateAAD(associatedData);
return cipher.doFinal(ciphertext);
}
/** SHA-256 prefix of the key material so admins can verify backups without exposing it. */
public String fingerprint() {
try {
byte[] digest = MessageDigest.getInstance("SHA-256").digest(key.getEncoded());
return HexFormat.of().formatHex(digest, 0, 8);
} catch (GeneralSecurityException e) {
return "unavailable";
}
}
}
@@ -0,0 +1,15 @@
package stirling.software.proprietary.storage.crypto;
import java.io.IOException;
/** Storage encrypt/decrypt failure (missing/disabled key, master-key mismatch, tampered blob). */
public class StorageEncryptionException extends IOException {
public StorageEncryptionException(String message) {
super(message);
}
public StorageEncryptionException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,112 @@
package stirling.software.proprietary.storage.crypto;
import java.time.Duration;
import java.util.function.Supplier;
import lombok.extern.slf4j.Slf4j;
import stirling.software.proprietary.storage.repository.FileEncryptionKeyRepository;
/**
* Holds the storage-encryption machinery for the always-installed {@link EncryptingStorageProvider}
* decorator.
*
* <p>The decorator is unconditional so a node whose config lags the cluster (flag off, rolling
* deploy, config drift) can never stream raw ciphertext: it always sniffs the header and decrypts
* or fails loudly. The expensive parts — resolving the master key (which may generate a key file)
* and the key service — are created lazily: eagerly at startup only when the write flag is on or
* key rows already exist (preserving the fail-fast master-key verification), otherwise on first
* encounter with an encrypted blob.
*/
@Slf4j
public class StorageEncryptionState {
private static final Duration KEYS_EXIST_CACHE_TTL = Duration.ofSeconds(60);
private final boolean writeEnabled;
private final Supplier<FileEncryptionKeyService> keyServiceFactory;
private final FileEncryptionKeyRepository keyRepository;
private volatile FileEncryptionKeyService keyService;
private volatile boolean keysExistEverChecked;
private volatile long keysExistCheckedAtNanos;
private volatile boolean keysExistCached;
public StorageEncryptionState(
boolean writeEnabled,
Supplier<FileEncryptionKeyService> keyServiceFactory,
FileEncryptionKeyRepository keyRepository) {
this.writeEnabled = writeEnabled;
this.keyServiceFactory = keyServiceFactory;
this.keyRepository = keyRepository;
}
/** Test convenience: a pre-materialised state around an existing service. */
public static StorageEncryptionState of(
boolean writeEnabled, FileEncryptionKeyService keyService) {
StorageEncryptionState state =
new StorageEncryptionState(writeEnabled, () -> keyService, null);
state.keyService = keyService;
return state;
}
/** True when new writes are encrypted (flag on + licence passed). */
public boolean isWriteEnabled() {
return writeEnabled;
}
/**
* The key service, created on first use. A failure here (no key material, wrong key) is a loud,
* actionable error — never silently-served ciphertext.
*/
public FileEncryptionKeyService keyService() throws StorageEncryptionException {
FileEncryptionKeyService current = keyService;
if (current != null) {
return current;
}
synchronized (this) {
if (keyService == null) {
try {
keyService = keyServiceFactory.get();
} catch (RuntimeException e) {
throw new StorageEncryptionException(
"Encrypted content was encountered but the storage encryption key"
+ " machinery could not be initialised: "
+ e.getMessage(),
e);
}
}
return keyService;
}
}
/** Forces eager initialisation (startup fail-fast path). */
public void initialiseEagerly() {
keyService = keyServiceFactory.get();
}
/**
* Whether presigned/direct download URLs must be suppressed: any time this node encrypts new
* writes or encrypted content may exist, a presigned URL could hand ciphertext to the client.
* The registry check is cached briefly, so installs that never enable encryption keep the S3
* fast path. Per-file precision (redirecting plaintext blobs via {@code
* StoredFile.encryptionKeyId}) is a deliberate follow-up.
*/
public boolean suppressDirectDownloads() {
if (writeEnabled || keyService != null) {
return true;
}
if (keyRepository == null) {
return false;
}
long now = System.nanoTime();
if (keysExistEverChecked
&& now - keysExistCheckedAtNanos < KEYS_EXIST_CACHE_TTL.toNanos()) {
return keysExistCached;
}
keysExistCached = keyRepository.count() > 0;
keysExistCheckedAtNanos = now;
keysExistEverChecked = true;
return keysExistCached;
}
}
@@ -0,0 +1,14 @@
package stirling.software.proprietary.storage.crypto;
/**
* The encryption key protecting the requested content is disabled (the per-scope kill switch).
* Distinct from a generic {@link StorageEncryptionException} so callers can surface this as an
* access-denied (403) rather than an internal error (500): it is a deliberate, reversible policy
* state, not a failure. Re-enabling the key restores access.
*/
public class StorageKeyRevokedException extends StorageEncryptionException {
public StorageKeyRevokedException(String message) {
super(message);
}
}
@@ -0,0 +1,95 @@
package stirling.software.proprietary.storage.model;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.UUID;
import org.hibernate.annotations.CreationTimestamp;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* A scope-level key-encryption key (KEK) for storage encryption at rest. The 256-bit KEK is stored
* AES-GCM-wrapped by the master file-encryption key; per-file data keys are wrapped by the KEK
* inside each encrypted blob's header, so this table stays tiny (one row per scope per rotation)
* and master-key rotation only re-wraps these rows, never file contents.
*
* <p>Status semantics: ACTIVE wraps new files and unwraps existing ones; RETIRED (post-rotation)
* unwraps only; DISABLED unwraps nothing — the per-scope kill switch.
*/
@Entity
@Table(
name = "file_encryption_keys",
indexes = {@Index(name = "idx_file_enc_keys_scope", columnList = "scope_type,scope_id")},
uniqueConstraints =
@UniqueConstraint(
name = "uk_file_enc_keys_scope_version",
columnNames = {"scope_type", "scope_id", "key_version"}))
@NoArgsConstructor
@Getter
@Setter
public class FileEncryptionKey implements Serializable {
private static final long serialVersionUID = 1L;
public enum ScopeType {
GLOBAL,
TEAM,
SOURCE
}
public enum Status {
ACTIVE,
RETIRED,
DISABLED
}
@Id
@Column(name = "key_id", nullable = false)
private UUID keyId;
@Enumerated(EnumType.STRING)
@Column(name = "scope_type", nullable = false, length = 16)
private ScopeType scopeType;
/** Scope discriminator (team id, source id). 0 for GLOBAL so the unique constraint holds. */
@Column(name = "scope_id", nullable = false)
private long scopeId;
/** Increments when the scope KEK is rotated; the unique constraint tolerates retired rows. */
@Column(name = "key_version", nullable = false)
private int keyVersion;
/** Base64 of IV(12) || AES-256-GCM(masterKey, kek) || tag(16), AAD-bound to keyId. */
@Column(name = "wrapped_key", nullable = false, length = 512)
private String wrappedKey;
/** Which master-key version wrapped this row; supports master rotation re-wraps. */
@Column(name = "master_key_version", nullable = false)
private int masterKeyVersion;
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false, length = 16)
private Status status;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@Column(name = "status_changed_at")
private LocalDateTime statusChangedAt;
@Column(name = "status_changed_by")
private String statusChangedBy;
}
@@ -102,6 +102,14 @@ public class StoredFile implements Serializable {
@Enumerated(EnumType.STRING)
private FilePurpose purpose;
/**
* file_encryption_keys id under which this file's blobs (main/history/audit) were encrypted;
* null = stored plaintext. The per-blob truth is each blob's own header — this column exists
* for reporting and the encrypt-existing migration. Nullable so ddl-auto upgrades cleanly.
*/
@Column(name = "encryption_key_id", length = 36)
private String encryptionKeyId;
@OneToMany(
mappedBy = "file",
fetch = FetchType.LAZY,
@@ -4,10 +4,15 @@ import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
@Builder(toBuilder = true)
public class StoredObject {
private final String storageKey;
private final String originalFilename;
private final String contentType;
/** Plaintext size. When encryption at rest is active the stored blob is larger. */
private final long sizeBytes;
/** file_encryption_keys id that wraps this blob's data key; null when stored plaintext. */
private final String encryptionKeyId;
}
@@ -0,0 +1,21 @@
package stirling.software.proprietary.storage.repository;
import java.util.Optional;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import stirling.software.proprietary.storage.model.FileEncryptionKey;
@Repository
public interface FileEncryptionKeyRepository extends JpaRepository<FileEncryptionKey, UUID> {
Optional<FileEncryptionKey> findFirstByScopeTypeAndScopeIdAndStatus(
FileEncryptionKey.ScopeType scopeType, long scopeId, FileEncryptionKey.Status status);
Optional<FileEncryptionKey> findFirstByScopeTypeAndScopeIdOrderByKeyVersionDesc(
FileEncryptionKey.ScopeType scopeType, long scopeId);
Optional<FileEncryptionKey> findFirstByStatus(FileEncryptionKey.Status status);
}
@@ -32,6 +32,7 @@ import stirling.software.common.model.ApplicationProperties;
import stirling.software.proprietary.security.database.repository.UserRepository;
import stirling.software.proprietary.security.model.User;
import stirling.software.proprietary.security.service.EmailService;
import stirling.software.proprietary.storage.crypto.StorageKeyRevokedException;
import stirling.software.proprietary.storage.model.FileShare;
import stirling.software.proprietary.storage.model.FileShareAccess;
import stirling.software.proprietary.storage.model.FileShareAccessType;
@@ -143,6 +144,7 @@ public class FileStorageService {
storedFile.setContentType(mainObject.getContentType());
storedFile.setSizeBytes(mainObject.getSizeBytes());
storedFile.setStorageKey(mainObject.getStorageKey());
storedFile.setEncryptionKeyId(mainObject.getEncryptionKeyId());
applyHistoryMetadata(storedFile, historyObject);
applyAuditMetadata(storedFile, auditObject);
try {
@@ -207,6 +209,7 @@ public class FileStorageService {
existing.setContentType(mainObject.getContentType());
existing.setSizeBytes(mainObject.getSizeBytes());
existing.setStorageKey(mainObject.getStorageKey());
existing.setEncryptionKeyId(mainObject.getEncryptionKeyId());
if (historyObject != null) {
applyHistoryMetadata(existing, historyObject);
}
@@ -492,6 +495,17 @@ public class FileStorageService {
ensureStorageEnabled();
try {
return storageProvider.load(file.getStorageKey());
} catch (StorageKeyRevokedException e) {
// Deliberate, reversible policy state (encryption key disabled), not a server fault —
// surface as forbidden so the client sees "revoked", not "internal error".
log.warn(
"Access to stored file {} denied: {}",
file != null ? file.getId() : null,
e.getMessage());
throw new ResponseStatusException(
HttpStatus.FORBIDDEN,
"Access to this file has been revoked (its encryption key is disabled)",
e);
} catch (IOException e) {
log.error(
"Failed to load stored file {} (key: {})",
@@ -263,6 +263,7 @@ public class WorkflowSessionService {
storedFile.setContentType(storedObject.getContentType());
storedFile.setSizeBytes(storedObject.getSizeBytes());
storedFile.setStorageKey(storedObject.getStorageKey());
storedFile.setEncryptionKeyId(storedObject.getEncryptionKeyId());
storedFile.setPurpose(purpose);
return storedFileRepository.save(storedFile);
@@ -298,6 +298,7 @@ class ExternalApiCallControllerLiveTest {
private String headers;
private boolean includeContext;
private boolean includeFile = true;
private long maxRequestBytes = 0;
private String policyName;
private String runId;
@@ -356,6 +357,11 @@ class ExternalApiCallControllerLiveTest {
return this;
}
Step maxRequestBytes(long v) {
maxRequestBytes = v;
return this;
}
Step run(String policy, String id) {
policyName = policy;
runId = id;
@@ -380,6 +386,7 @@ class ExternalApiCallControllerLiveTest {
headers,
includeContext,
includeFile,
maxRequestBytes,
policyName,
runId);
}
@@ -389,6 +396,15 @@ class ExternalApiCallControllerLiveTest {
return new Step();
}
@Test
void aDocumentOverTheSizeLimitFailsBeforeTheCall() {
connection(Map.of());
// A 1-byte cap is under the test PDF, so the step stops before any request goes out.
assertThatThrownBy(() -> step().path("/v1/scan").maxRequestBytes(1L).go())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("limit set for this step");
}
@Test
void sendsTheDocumentAndWhatWeKnowAboutItToTheReceiver() throws IOException {
connection(Map.of());
@@ -17,6 +17,7 @@ import stirling.software.common.configuration.RuntimePathConfig;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.proprietary.policy.model.InputSpec;
import stirling.software.proprietary.policy.model.OutputSpec;
import stirling.software.proprietary.policy.model.PipelineInput;
import stirling.software.proprietary.policy.model.Policy;
import stirling.software.proprietary.policy.source.InProcessSourceStore;
import stirling.software.proprietary.policy.source.Source;
@@ -189,6 +190,13 @@ class FolderAccessGuardTest {
null))
.id())
.toList();
return new Policy("p1", "p", "owner", true, null, sourceIds, List.of(), output);
return new Policy(
"p1",
"p",
"owner",
true,
sourceIds.stream().map(PipelineInput::manual).toList(),
List.of(),
output);
}
}
@@ -90,6 +90,6 @@ class PolicyAccessGuardTest {
private static Policy inTeam(Long teamId) {
return new Policy(
null, "p", "owner", true, null, List.of(), List.of(), OutputSpec.inline(), teamId);
null, "p", "owner", true, List.of(), List.of(), OutputSpec.inline(), teamId);
}
}

Some files were not shown because too many files have changed in this diff Show More