# Description of Changes
## The problem
`useAdminSettings` backs all 18 admin config sections. Each section
fetched its own copy of its settings block, held it in hand-rolled
loading/saving state, and refetched manually after every save.
Three consequences:
- **Duplicate fetching.** Four AI tabs all read the `aiEngine` block.
Nothing was shared, so each open refetched it.
- **Duplicated wiring.** All 18 sections carried the same effect to
trigger the fetch, each one depending on a `fetchSettings` callback that
would have refetched on every render had it ever become unstable.
- **Console noise.** The hook made 11 `console.*` calls, four of them
`JSON.stringify(settings, null, 2)` on **every fetch and every save** —
admin configuration serialised into the console of every admin session.
Every save also ended with a hand-written `await fetchSettings()`.
Forget it in a new section and its pending badges silently go stale.
## The fix
The hook uses TanStack Query, keyed on `sectionName`, so sections
reading the same block share one fetch and one cache entry.
The fetch gate moved into the hook. Sections used to write:
```ts
const { settings, fetchSettings } = useAdminSettings({ sectionName: "legal" });
useEffect(() => {
if (loginEnabled) fetchSettings();
}, [loginEnabled, fetchSettings]);
```
and now write:
```ts
const { settings } = useAdminSettings({
sectionName: "legal",
enabled: loginEnabled,
});
```
Saving is a mutation that invalidates the section on success, so the
refetch is structural rather than something each section remembers.
The delta computation and the save transformer are unchanged — that is
domain logic, not fetching. `settings` is still an editable draft seeded
from the server response, so forms behave exactly as before.
## Why it is better
Measured against the previous implementation across identical scenarios.
`commits` counts committed renders.
| Scenario | Before | After |
|---|---|---|
| Open one section | 2 commits, 1 request | 2 commits, 1 request |
| Browse the four AI tabs | 8 commits, 4 requests | **5 commits, 1
request** |
| Edit and save | 4 commits, 2 requests | 4 commits, 2 requests |
Committed renders are equal or better everywhere; browsing the AI tabs
costs a quarter of the requests.
The diff reads +449 / −303, but that includes a test file for a hook
that had no tests:
| | Added | Removed | Net |
|---|---|---|---|
| Production code (21 files) | 154 | 303 | **−149** |
| Tests (1 file) | 295 | 0 | +295 |
The 18 section files account for −133 of that: each drops an effect, a
destructure and usually an import, and gains one `enabled:` line. The
hook itself goes from 234 to 180 lines. `console.*` calls go from 11 to
0.
## Caching
Settings inherit the client's 30s stale window rather than refetching on
every mount, which is where the request saving comes from.
Nothing inside a cached block is server-observed — the only live reads
in these sections, `/api/v1/ai/health` and the tessdata language list,
are separate calls outside this query. A block therefore only changes
when another admin writes it.
Two things bound the staleness:
- Sections already held a single snapshot for as long as the modal
stayed open, with no refetch on focus. 30s is shorter than that window,
not longer.
- `computeDelta` only emits fields whose draft differs from the baseline
it was seeded from, so a stale baseline cannot produce a collateral
write. The only race is two admins editing the same field, which is
unchanged. Saving invalidates, so acting refreshes to current values.
The blocks where a stale read would matter most — `security`, `premium`,
`database` — are set once at deployment and effectively never edited
concurrently. The block with the most cache reuse, `aiEngine`, is the
least consequential.
**Convention:** config blocks cache; observed state does not. A section
that displays live server state inside its settings block should
override `staleTime` locally.
## Testing
14 tests, covering the shared fetch, cache reuse across tab reopens, key
separation between blocks, the `enabled` gate, delta-only saves, the
empty-delta short circuit, post-save invalidation, pending-value
display, and draft reseeding.
Each was checked by breaking the implementation and confirming the suite
fails: per-consumer query keys, sending the whole draft instead of the
delta, dropping the post-save invalidate, reporting loaded while
disabled, skipping the empty-delta short circuit, and reverting the
stale window to zero.
`task frontend:check` green. Two unrelated tests fail on this branch —
`workbenchSession.test.ts` and `notificationActions.test.tsx` — and fail
identically on `main`.
## Follow-ups
The sections that fetch through services rather than this hook — Teams,
TeamDetails, People, roughly 2,600 lines — are unchanged. Between them
they share two reads (`getTeams` and `getUsers`, both used by all three)
and carry ten distinct write operations, with no test coverage today.
---
## Primer: mutations
`useQuery` is for reads. It caches, dedupes, and re-renders when data
arrives. `useMutation` is for writes, where none of that applies — a
write happens once, when the user asks.
```ts
const save = useMutation({
mutationFn: (body) => putAdminSection("legal", body),
onSuccess: () => queryClient.invalidateQueries({ queryKey }),
});
save.mutate(body); // fire and forget
await save.mutateAsync(body); // or await it
save.isPending; // disable the button
save.error; // show the failure
```
`isPending` and `error` replace the `useState` flag and
`try/catch/finally` you would otherwise write around every save.
After a write the cache holds stale data. Two ways to fix it:
| | What it does | Use when |
|---|---|---|
| `invalidateQueries` | Marks the data stale so it refetches | The
server may transform, queue or reject part of what you sent |
| `setQueryData` | Writes your value into the cache, no request | The
response tells you exactly what the server now holds |
**Invalidate by default. Use `setQueryData` only when the response is
authoritative.**
This hook has to invalidate: the server can queue a settings change
rather than applying it, returning it in a `_pending` block that the
form renders as a badge. Writing the local draft into the cache would
show a queued change as applied.
Most mutations are not like that. A "rename a team" write, where the
response is the new team, is a `setQueryData` case.
One gotcha: `mutate` does not throw, `mutateAsync` does. An awaited
`mutateAsync` without a `try/catch` is an unhandled rejection.
Copy only. No behaviour, no lookup keys, no licence semantics, no
backend.
## Current state
Every surface that sells the paid self-hosted tier offers **"unlimited
seats"** for **"$99/server/mo"**, and the portal's free plan badges
**"Unlimited users"** and **"SSO included"** as free-tier facts.
## Problem
Both claims are now enforceably false.
[#7492](https://github.com/Stirling-Tools/Stirling-PDF/pull/7492) makes
the licence carry a real user cap, and
[Stirling-PDF-SaaS#325](https://github.com/Stirling-Tools/Stirling-PDF-SaaS/pull/325)
sells capacity in blocks of 100 users. An admin reading "unlimited
seats" and then hitting a 409 at the invite screen is the worst version
of this.
The demo has already dropped both claims; ours were the last ones
standing.
## Solution
| Surface | Was | Now |
|---|---|---|
| Onboarding licence slide | "Stirling Server plan, **unlimited seats**
… $99/server/mo" | "Stirling Team plan, **100 users** … $99/mo" |
| Plan comparison table | `unlimitedUsers` = "Unlimited users" |
`usersIncluded` = "100 users included" |
| Plan card highlights | "Unlimited users" | "100 users included" |
| Static plan section | `name: "Server"`, `maxUsers: "Unlimited users"`
| `plan.team.name`, `plan.team.maxUsers` |
| Upgrade banner | "Upgrade to Server Plan" / "unlimited users" |
"Upgrade to the Team plan" / "100 users, SSO" |
| Portal free plan | "Editor" + "SSO included" + "Unlimited users" |
"Editor" + "Every PDF tool" + "Web, desktop & self-hosted" |
The i18n keys are **renamed** (`unlimitedUsers` to `usersIncluded`)
rather than just revalued, so the key name cannot outlive the claim.
Also drops "per server" from `plan.licenseWarning` — we price a block of
100 users and count the provisioned roster, never nodes. And deletes the
orphaned `[settings.planBilling.tier]` block: zero source references,
and it described a retired model (50 credits/mo free, 500 included plus
overage billing).
## Deliberately unchanged
**"Processor" stays the name of the product surface.** The demo names
each plan for its price tier (Editor = $0, Team = $99/mo, Credits = 1¢
each) while keeping Processor as the surface a plan unlocks. Renaming
the surface here would conflate the two, so the plan-name split is left
for the explicit plan catalogue. The free plan also gains no "500 free
credits monthly" badge yet: that is true in the demo but not in our
backend, which still grants a one-time lifetime pool.
## How to test
Self-hosted, as an admin over the free user limit: Settings → Plan
should offer the Team plan at "100 users included", and the onboarding
licence slide should no longer promise unlimited seats. On the portal
billing page, the free plan should read "Free" with no SSO or
unlimited-users badge.
Green locally: 4/4 i18n audits (missing, unused, structure,
translation), 876 tests across 110 files, oxlint, prettier, and all four
typecheck variants (core, proprietary, saas, portal).
Review Flow PR 5a — the first half of #7479, which stays open for
reference until both halves land. This PR is the ranking and the
bookkeeping; #7762 adds the retry handlers. Merging both reproduces
#7479's diff byte-for-byte.
## What's added
**The action slot model (backend).** `FailureActionSlot` ranks each of a
kind's offers as its `RESOLUTION`, `SECONDARY` or `OVERFLOW`.
`FailureKind` now declares placement per offer — the password-protected
kind names `DECRYPT_AND_RETRY` as its resolution, `UNKNOWN` leads with a
plain `RETRY` — and `FailureActionId` gains those two ids. The
declarations are data; their client handlers arrive in the follow-up, so
this build withholds them with a reason rather than rendering unwired
buttons (the same forward-compatibility #7478 relied on).
**A resolve transition.** `POST /api/v1/notifications/{id}/resolved`
lets a client report a failure fixed. `NotificationSource.parse` turns a
qualified notification id back into the source that owns it, and
`FileRunEventService` folds the resolution into the incident rather than
deleting it.
**`viewerReviewsTeam` on the list response.** A member sees only rows
whose document this browser holds — they can neither open nor fix
anything else — while a team reviewer keeps every row.
**The bell renders the ranking** (`promoteActions`): one primary button,
at most one secondary, the rest in an overflow menu beside **Copy log**.
The row's body is the kind's own sentence; the raw failure message moves
into the menu.
**Read state is a timestamp, not a row id.** `readThroughAt` replaces
`lastSeenId`: when a resolved or dismissed row leaves the list, the rows
below it stay read instead of re-lighting the badge.
## How to test
Needs a proprietary or SaaS build with login enabled (`task dev:all`,
sign in).
1. **Create a failure.** Add a password-protected PDF to the editor and
choose **Skip for now**; the upload's policy run fails on it.
2. **Open the bell.** The row reads the kind's sentence, not a stack
trace. Its primary button is **View file** — the server offers Decrypt
and retry as the resolution, but this build withholds it (handler lands
in the follow-up), so the best renderable offer is promoted instead.
3. **Open the row's ⋯ menu.** View in processor and Dismiss sit there,
along with **Copy log**, which copies the raw message.
4. **Check the read marker survives a departure.** With two failures,
open the bell (badge clears), dismiss the newer row, and refresh: the
badge stays dark. On main, the marker held the departed row's id and the
older row re-read as unread.
5. **Member visibility.** As a plain member, a failure recorded from
another browser does not appear in the bell; as a team reviewer it does.
6. **Resolve endpoint.** `POST
/api/v1/notifications/failure-{eventId}/resolved` as the owner removes
the row on the next poll; `NotificationResolveTest` pins refusal for a
non-owner, an unknown id, and a foreign prefix.
## Migration
None.
Redesigns the policies system so that the backend has an understanding
of policies running over the Editor. The Editor is not set up as a
source for the backend because the backend can't actively get files from
it, they come in via the frontend sending them to the backend, so
instead pipelines have a specific editor key in them to encode whether
the pipeline is triggered on file upload/export in the editor.
Also make a big effort in the frontend code towards genericising policy
running. Previously, there was specific support in the main policy
executor for each policy that it had to run, which was not going to be
appropriate long-term, especially when users can run any pipeline in the
editor. There's more work needed here for me to really be happy with it
but this PR is plenty large on its own and moves it in the right
direction.
All of the above was required to allow arbitrary user pipelines to run
in the editor. This PR makes it so that the user can select Editor as a
source in the pipeline creator, along with whether it should run on
upload or export.
<img width="1437" height="506" alt="image"
src="https://github.com/user-attachments/assets/b2d176a1-185c-480b-9916-abdd1447d8e1"
/>
---------
Co-authored-by: James Brunton <james@stirlingpdf.com>
Replaces the bare account-link login box with a guided Connect flow, and
wires up the triggers that actually put it in front of someone.
## Top bar
<img width="1580" height="422" alt="image"
src="https://github.com/user-attachments/assets/719e12fc-121a-4caa-bc72-124c5167b011"
/>
## The modal
Three steps on the portal's own `FlowModal` + `StepModalHeader`, the
shells procurement and prepay already wear:
1. **What you unlock** — six benefits as a plain list.
<img width="817" height="503" alt="image"
src="https://github.com/user-attachments/assets/4644ddd2-6181-44e1-9be9-7a961972195d"
/>
2. **Sign in** — the existing `SupabaseLoginForm`, reseated.
<img width="880" height="930" alt="image"
src="https://github.com/user-attachments/assets/fc66cbbb-9f98-40a4-9daa-4f2447713f39"
/>
3. **Connected** — confirms, then deep links into Users, Pipelines and
Policies.
<img width="876" height="752" alt="image"
src="https://github.com/user-attachments/assets/28358e4d-a44f-4118-a8ae-8275984ebd00"
/>
Re-auth stays a single step with no pitch and no success screen.
## The triggers
**`LinkGate` stops being dead code.** It was built as the drop-anywhere
"link to unlock" wrapper and was imported by nothing. It is now a
blocking empty state that replaces the feature it guards, wired into
Pipelines, Policies, Users, Sources and Integrations.
**Scoped to creating and editing, never viewing.** Existing pipelines,
policies, sources and connections keep listing and running, so upgrading
an unlinked instance cannot take away something that already works. The
clicks that would open a builder or a create modal ask for the
connection first, which is the moment an admin has already declared
intent.
## Capability signal
`accountLinkAvailable` on `/api/v1/config/app-config`. Gating needs two
facts: whether the instance is linked (`LinkContext`) and whether it
*could* be (this flag). The account-link endpoints 404 when the feature
flag is off, which the client cannot distinguish from "not linked yet" —
so gating on link state alone would lock all five views on every default
install with no way out. `useConnectGate` holds that decision in one
place and shares the app-config query key, so it costs no extra request.
Read from the environment rather than `AccountLinkProperties` because
`:core` cannot depend on `:proprietary`.
# Description of Changes
## The problem
The portal's query client was created per mount:
```ts
const [queryClient] = useState(createPortalQueryClient);
```
The portal is a route (`/processor/*`, a lazy element), and the switch
to the editor is a client-side `navigate()`. So leaving the processor
unmounts `PortalApp`, the client goes with the component, and the cache
goes with the client. Coming back refetches everything, whether or not
anything changed: four requests for the Users page alone (roster,
grants, teams, auth config), and 21 `useQuery` sites across the portal.
The editor's client sits above the router in `AppProviders` and survives
the same trip. The round trip only ever cost in one direction.
## The fix
The module already kept the instance in a module-level slot so
`tryGetPortalQueryClient()` could find it. It just replaced it on every
mount instead of reusing it, so the change is to create it lazily and
hand out the same one:
```ts
export function getPortalQueryClient(): QueryClient {
current ??= new QueryClient({ defaultOptions: { queries: baseQueryOptions } });
return current;
}
```
Still a separate instance from the editor's. The two namespace their
keys apart (`["portal", ...]` against `["editor", ...]`) and invalidate
independently, which this does not change.
## What this does not do
`gcTime` is 5 minutes, from the shared `baseQueryOptions`. An entry with
no observer is still collected on that timer, so this warms a quick trip
to the editor and back, not a return after a long editing session.
Raising the portal's `gcTime` is a separate decision and is not made
here.
## Why it is safe
**Signing out.** A cache that outlives a mount must not outlive a
session, because the portal's holds the admin roster, emails and roles.
Logout goes through `window.location.assign`, a full page load, so the
whole JS context is discarded and no cache can survive it. Nothing in
the codebase calls `queryClient.clear()` on sign-out, and nothing needs
to. If logout ever becomes a client-side navigation, this needs an
explicit reset, and `resetPortalQueryClient()` is the hook for it.
**The one caller of the null check.** `resolveTeam` in
`saas/portal/usersBackend.ts` uses `tryGetPortalQueryClient()` and falls
back to a direct fetch when there is no client, which its comment
describes as the unit-test path; the cache path is preferred because it
honours both `staleTime` and invalidation. A longer-lived client means
the preferred path is taken more often, not less.
## Testing
Three tests in `queryClient.test.tsx`, and the first two fail if the
client goes back to being created per call:
| | |
|---|---|
| A remount is served from cache rather than refetching | the behaviour
this changes |
| Every caller gets the same instance | the mechanism |
| No client is reported until the portal first mounts | the contract
`resolveTeam` reads |
The three existing portal caching suites called the factory expecting a
fresh client per case. They now call `resetPortalQueryClient()` in a
`beforeEach`, which is what keeps `sharing.test.tsx`'s "a later screen
refetches nothing" case honest rather than passing on a leaked cache.
`task frontend:check` passes typecheck, lint and oxfmt, and 2402 of 2404
editor tests. The two failures, `workbenchSession.test.ts` and
`notificationActions.test.tsx`, are untouched here and fail the same way
on `main`.
> [!WARNING]
> Cooldown could not be applied because no publication date was
available from the registry.
>
Bumps the eclipse-temurin group with 1 update in the /docker/backend
directory: eclipse-temurin.
Bumps the eclipse-temurin group with 1 update in the /docker/base
directory: eclipse-temurin.
Bumps the eclipse-temurin group with 1 update in the /docker/embedded
directory: eclipse-temurin.
Updates `eclipse-temurin` from `fbcf915` to `b4c93a5`
Updates `eclipse-temurin` from `fbcf915` to `b4c93a5`
Updates `eclipse-temurin` from `fbcf915` to `b4c93a5`
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>
> [!WARNING]
> Cooldown could not be applied because no publication date was
available from the registry.
>
Bumps the ubuntu group with 1 update in the /docker/base directory:
ubuntu.
Bumps the ubuntu group with 1 update in the /docker/unoserver directory:
ubuntu.
Updates `ubuntu` from `561618e` to `33ceb71`
Updates `ubuntu` from `561618e` to `33ceb71`
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Auto-generated by stirlingbot[bot]
This PR updates the frontend license report based on changes to
package.json dependencies.
Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
# Description of Changes
This PR removes workflow-specific cache suffixes from Python dependency
caching in several CI workflows.
Previously, the following workflows appended their own `cache-suffix`
even though they use the same Python dependency files:
- `ai-engine.yml`
- `check-generated-models.yml`
- `pre_commit.yml`
- `sync_files_v2.yml`
All of these workflows use the same cache dependency inputs:
- `engine/pyproject.toml`
- `engine/uv.lock`
The workflow-specific suffixes caused separate cache entries to be
created for effectively identical dependency sets. This resulted in
unnecessary cache duplication and reduced cache reuse between workflows.
By removing the suffixes, these workflows can now share the same cache
when their dependency inputs and other cache key components match.
This change reduces redundant cache storage, improves cache hit
potential across CI workflows, and avoids repeatedly creating equivalent
caches under different names.
No functional application behavior is changed. The modification only
affects CI cache key generation and reuse.
---
## 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.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bumps the iconify group with 1 update in the /frontend directory:
[@iconify-json/material-symbols](https://github.com/iconify/icon-sets).
Updates `@iconify-json/material-symbols` from 1.2.83 to 1.2.89
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/iconify/icon-sets/commits">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>
# Description of Changes
Step 4 of the TanStack Query rollout, and the first of the polling
hooks. Follows #7264, #7283, #7285.
## The problem
`useSigningSessions` hand-rolled its own fetch, loading state and
`setInterval`. Two consequences:
- **A raw `setInterval` keeps polling a hidden tab.** Browsers throttle
background timers, they do not stop them, so a backgrounded editor with
Shared Sign open keeps hitting both endpoints for as long as it is open.
- **No tests.** The hook had none, and its quietest behaviour (below) is
the easiest thing to break without noticing.
## End state
One query behind `qk.signingSessions()`, with the polling lifecycle
handed to the library:
- Polling stops while the tab is hidden, and refetches on return rather
than leaving data up to a full interval stale.
- Mounts render from cache while they revalidate, so moving between the
tool picker and the signing tool no longer flashes an empty list.
- 12 tests where there were none.
Same return shape, so no consumer files change.
### What this is not
This is not a deduplication win. The three consumers are never mounted
at the same time: `ToolPanel` renders the tool picker or the active tool
and never both, so the badge cannot be on screen with either of the
others, and `SharedSigningLauncher` and `useSigningSessionController`
sit inside two different tools. The shared key earns its keep on cache
reuse across those transitions, not on concurrent fetches.
## The bit worth reviewing
The hand-rolled `{ silent: true }` flag encoded three states, and no
single Query flag reproduces them:
| | Spinner | Toast on failure |
|---|---|---|
| First load | yes | yes |
| Background poll | no | no |
| Explicit refetch | **yes** | **yes** |
`isLoading` is false during an explicit refetch when data is already on
screen; `isFetching` is true during a background poll. Neither matches,
so the user-initiated case is tracked with a small flag and the failure
toast is gated on `isLoadingError` plus the explicit path.
## Testing
Twelve tests. Rather than trust them, each claim was checked by breaking
the implementation and confirming the relevant test fails:
| Mutation | Caught by |
|---|---|
| `refetchIntervalInBackground: true` | hidden-tab test |
| Drop `refetchOnWindowFocus` | returns-to-view test |
| Drop the user-initiated spinner flag | manual-refresh test |
| Toast on every error | background-failure-is-silent test |
| Give each observer its own key | dedupe test |
Three things worth knowing for the next conversion:
- **`waitFor` flushes renders.** Recording an index *after*
`waitFor(callCount === 2)` skips past the in-flight render, so a "did
the spinner flip on" assertion passes vacuously. The marker has to go
before the poll.
- **Fake timers hide in-flight state.** The fetch settles inside the
same `act()`, so the intermediate render never happens. That test uses
real timers and a held-open promise.
- **`visibilitychange` has to bubble.** query-core listens for it on
`window`, and the real event bubbles from `document`. A test helper
dispatching a non-bubbling event never reaches the focus manager, and
the pause behaviour still appears to work because `refetchInterval`
reads `document.visibilityState` directly at tick time rather than
through the event.
**One claim is deliberately unguarded.** `isLoading` vs `isFetching` for
a background poll produces no re-render at all, so there is nothing
observable for a test to assert and no user-visible difference to
protect.
## Pre-existing failures
`task frontend:check` passes typecheck, lint and oxfmt, and 2363 of 2365
editor tests. The two failures, `workbenchSession.test.ts` and
`notificationActions.test.tsx`, fail identically with this branch's
changes reverted and are untouched by it.
## Scope
This is one of five pollers. The remaining four, `useLocalFolderPoller`,
`WatchedFolderWorkbenchView`, `SessionDetailPanel` and cloud
`TeamSection`, are separate files with their own consumers and follow
separately, now that the silent-refresh pattern has a worked example.
---------
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
# Description of Changes
Step 5 of the TanStack Query rollout, covering the admin People, Teams
and Team details screens. Follows #7264, #7283, #7285.
## The problem
Two separate ones, in the same three files.
**Reads.** Each section fetched and held its own copy of the same
resources: People read the roster and the team list, Teams read the team
list plus the roster again when its add-member modal opened, Team
details read all three. Cost scaled with how many screens you visited
rather than with how much data exists.
**Writes.** Thirteen handlers each did the same five things by hand: set
a processing flag, call the service, toast the outcome, dig a message
out of an axios error, and reload their own slice. Refreshing was a
convention, not a mechanism, and one handler had already forgotten it.
## The fix
Three shared query keys (`adminUsers`, `teams`, `teamDetails`), and one
`useAdminMutation` helper that every write is declared against:
```ts
const createTeam = useAdminMutation({
write: (name: string) => teamService.createTeam(name),
invalidates: ["teams"],
success: t("workspace.teams.createTeam.success"),
errorFallback: t("workspace.teams.createTeam.error"),
onDone: () => { setNewTeamName(""); setCreateModalOpened(false); },
});
```
Each write names the slices it disturbs, which is the part that only
works when reads and writes are designed together: `createTeam`
invalidates the team list, while a membership move invalidates the list,
both teams' detail rows and the roster, because it genuinely changes all
three. Invalidation refetches only mounted queries, so this costs
nothing extra.
The blanket "invalidate everything" helper survives in exactly one role:
child components (invite, password change, seat update) that write
through their own services, where the affected scopes are not visible
from the call site.
## Why it is better, measured
Request counts come from one harness driving `teams -> team details ->
back -> people`, run against the branch point and against this branch.
The assertion is committed, so it cannot silently regress.
| | Before | After |
|---|---|---|
| Requests | 7 | **3** |
| `getTeams` | 4 | **1** |
| `getUsers` | 2 | **1** |
| `getTeamDetails` | 1 | 1 |
| Committed renders | 17 | **15** |
Three is one per distinct resource, the floor for that sequence. The
four `getTeams` were the Teams table, Team details fetching the same
list for its "move to team" dropdown, the explicit refresh on the back
button, and People.
Renders barely move, which is expected: this changes where data lives,
not how often React draws. It is reported because a caching change can
quietly cost renders, and this one does not.
On the code itself, across the three sections:
| | |
|---|---|
| Net lines | **-216** |
| `useState`/`useEffect` removed | **11**, none added |
| Duplicated `isAxiosError` blocks | 13 to **1** |
| `setProcessing` calls | 19 to **0** |
`isAxiosError` is no longer imported by any of the three files.
## Bug fixed
`disableMfaByAdmin` showed a success toast and never refreshed. The menu
item renders only when `user.mfaEnabled` is true, so an admin disabled
MFA, was told it worked, and watched the option stay on screen until a
manual reload. It is covered by a test that fails if the invalidation is
removed.
## Behaviour worth checking in review
- A write no longer blocks its handler before closing the modal. The
dialog closes when the write succeeds and the table updates when the
refetch lands, rather than the button spinning through both.
- Modal submit buttons now track their own mutation rather than one
shared flag. Team details still derives a single busy flag, now from its
five mutations rather than a `useState`, so its row actions disable
together as before.
- The per-handler `console.error` is kept, once, in the shared error
path.
## Testing
Five tests, each verified by breaking the implementation and confirming
that one test, and only that one, fails:
| Mutation | Caught by |
|---|---|
| Drop the shared stale window (`staleTime: 0`) | request-count test |
| Make invalidation a no-op | write-visibility test |
| Ignore the login-enabled gate | login-disabled test |
| Stop invalidating after the MFA write | MFA-refresh test |
| Fall back to the generic error message | server-message test |
The write tests drive the real flows through their modals and menus
rather than calling hooks directly.
`task frontend:check` passes typecheck, lint and oxfmt, and 2383 of 2385
editor tests. The two failures, `workbenchSession.test.ts` and
`notificationActions.test.tsx`, are untouched here and fail identically
with this branch's changes reverted.
## Scope
The three services keep their current shape; nothing outside these three
sections and the new hook module changes. Child modals that write
through their own services still refresh via the blanket helper, and
converting those is separate work.
## What
Every dialog in the processor is the shared `.sui-modal` shell, and its
backdrop was top-aligning the panel:
```css
align-items: flex-start;
padding: 5rem 1.5rem 1.5rem; /* 80px above, 24px below */
```
On a 900px-tall viewport that started every dialog at `y=80` with ~350px
of dead space beneath it. Phones already had an `align-items: center`
override; desktop never got one.
## Change
`frontend/editor/src/core/ui/Modal.css` only:
- Symmetric block inset, `align-items: center`.
- The inset is published as `--modal-inset-block`, and `.sui-modal`'s
`max-height` derives from it. That coupling is the point: if the two
drift apart, a tall modal overflows a centre-aligned backdrop and loses
its header off the top of the screen, unreachable.
- The phone breakpoint now only moves the variable. Measured at 375x812
it resolves to exactly the previous values (`16px 12px`, `max-height:
780px`), so mobile behaviour is unchanged.
One shared file, so this covers flow modals, source / user / pipeline /
API-key modals, billing and procurement.
## Before / After
<img width="2104" height="2284" alt="image"
src="https://github.com/user-attachments/assets/bcb50145-f75e-449e-92c6-a0b085cc091c"
/>
## Testing
- `task frontend:check` passes (lint + typecheck + 2356 tests).
- Phone breakpoint measured directly in the browser, values match the
previous behaviour.
## The problem
AI PRs write comments that restate the line below them, mark sections
with box drawing, and narrate the diff. Nothing in the repo said not to,
and nothing checked. `AGENTS.md` had one line about comments and it was
buried in the Python section.
Banners and `Step N:` narration have zero occurrences in the 15 months
before Aug 2025, so this is new.
## The fix
A written standard, plus a linter that enforces the mechanical part of
it on added lines only.
-
[devGuide/CODE_COMMENTS.md](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/devGuide/CODE_COMMENTS.md)
holds the reasoning and worked examples; a section in
[AGENTS.md](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/AGENTS.md)
holds the operative rules, kept short so they stay in an agent's
context. The two are split by kind rather than duplicated, because the
same prose in two places drifts.
- Rules in
[comment-rules.mjs](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/scripts/lint/comment-rules.mjs),
shared by both engines.
- Two engines. `.ts` / `.tsx` / `.mjs` go to an [oxlint JS
plugin](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/scripts/lint/comment-lint-oxlint-plugin.mjs)
so comments come from the parser rather than a line scan; `.java` /
`.py` go to a [line
scanner](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/scripts/lint/comment-lint.mjs).
Neither reads the other's files, so they cannot disagree about one file.
- Between them they read every comment form the repo writes: `//` and
`/* */`, Javadoc and JSDoc, JSX comments, `#`, and Python docstrings.
- Runs in `task pre-commit`, so the git hook and the `pre_commit.yml` CI
job both get it, and as a Claude Code [`Stop`
hook](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/scripts/lint/comment-lint-hook.mjs)
so an agent fixes the comment inside the turn that wrote it.
## The rules
The part worth arguing about. **Every rule blocks.** A rule that only
warns is a rule nobody acts on, so a finding you believe is wrong is a
bug in the rule: narrow it, or mark the line and say why.
| | Fires on |
| --- | --- |
|
[CMT001](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/scripts/lint/comment-rules.mjs#L71)
| Every word in the comment already appears in the code below it. Max 6
words, skipped for prose punctuation and for a bare Arrange/Act/Assert
marker |
|
[CMT002](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/scripts/lint/comment-rules.mjs#L92)
| 4+ rule or box-drawing characters, or a bare section label from [a
fixed
list](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/scripts/lint/comment-rules.mjs#L84)
(`Types`, `Helpers`, `State`, `Handlers`, ...) |
|
[CMT003](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/scripts/lint/comment-rules.mjs#L110)
| `Step N:` with a separator, or `Then,` / `Next,` / `Finally,`.
Suppressed in test files |
|
[CMT004](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/scripts/lint/comment-rules.mjs#L129)
| A comment about the code's own past: `this used to`, `renamed from`,
`was previously called`. Suppressed in test files |
|
[CMT005](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/scripts/lint/comment-rules.mjs#L154)
| 3+ consecutive comment lines where 2/3 [parse as
code](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/scripts/lint/comment-rules.mjs#L143)
|
|
[CMT006](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/scripts/lint/comment-rules.mjs#L31)
| A run of implementation comment over 12 lines, outside the first 5
lines of a file. Doc blocks are exempt, because the standard asks for
thorough contracts |
|
[CMT007](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/scripts/lint/comment-rules.mjs#L180)
| A parameter or return description that adds no word its name lacks.
Reads Javadoc/JSDoc `@param`, Sphinx `:param name:` and Google `name:
description` |
|
[CMT008](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/scripts/lint/comment-rules.mjs#L239)
| An allow directive naming a rule that does not exist, or one that
silenced nothing |
|
[CMT009](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/scripts/lint/comment-rules.mjs#L219)
| A `TODO` / `FIXME` / `HACK` naming no issue or link. An owner is not
accepted: a username goes stale, an issue outlives it |
Each rule carries the readings it deliberately excludes, next to the
rule. Those exclusions came from running the rules over this repo, not
from taste: `CMT004` does not match a bare "no longer needed" because
that is as often about runtime lifecycle as about history, and `CMT003`
needs a separator after the number so a wrapped line beginning "step 2
unmounts + remounts the panel" reads as the prose it is.
A comment sharing a line with code is judged by the rules that do not
depend on the code below it, so a trailing `// TODO fix this` or `/*
this used to run before the flush */` still reports, while `50L * 1024 *
1024 // 50 MB` does not. `CMT001` would have been wrong about six in
seven trailing comments here, so it stays out of them.
If a finding is wrong, `// comment-lint-allow: CMT002` on the line
above. Rule-specific, [no blanket
disable](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/scripts/lint/comment-rules.mjs#L229).
A directive naming a rule that does not exist, or silencing nothing, is
itself a `CMT008` failure, so a typo cannot quietly disable a rule and a
stale one gets deleted rather than accumulating.
No native linter covers `CMT007`. `eslint-plugin-jsdoc`'s
`require-param-description`, Checkstyle's `NonEmptyAtclauseDescription`
and ruff's D-rules all check that a description exists, not whether it
says anything.
## Scoping
Added comment **text** only, not lines git calls new. Reindenting a file
or moving a block makes git mark untouched comments as added; findings
are matched against the comment text at the base, so only genuinely new
content reports.
The whole file is read and every comment in it evaluated. Only the
*reporting* is filtered, so a rule still sees the code a comment
introduces, the full run it belongs to, and the base version of the
file.
Existing tree is untouched. `task pre-commit:comment-lint:all` reports
it and always exits 0:
| | java | ts/js | py |
| --- | --- | --- | --- |
| findings | 1,218 | 741 | 204 |
2,163 across 542 files, mostly `CMT002` banners (1,482) and `CMT001`
restatements (456). Clearing it is separate work, by directory.
Not in this PR: an advisory LLM review layer for the things no pattern
can judge.
## Verification
Run against
[#7494](https://github.com/Stirling-Tools/Stirling-PDF/pull/7494) as CI
would, in a throwaway worktree: **two findings on a 78 file, +4,512 line
change, both genuine banners, in 952ms**. A whole-file scan of those
same files gives 11; the other 9 were withheld because that PR's author
did not write them, and they are the `@param teamId the team ID` shape
this standard exists to stop.
Both scanners blank string and character literals before looking for
comment markers, because a partial lex desynchronises everything after
it: one apostrophe in a Java comment, or one Python template whose
closing quotes start a line, is enough to read dozens of lines of code
as a single comment. Two fixtures carry canaries that stop being
reported if either engine ever desynchronises again.
The [fixture
corpus](https://github.com/Stirling-Tools/Stirling-PDF/tree/claude/ai-pr-comment-quality-dd970e/scripts/lint/fixtures)
pins all 9 rules against both engines, and `--selftest` fails if the two
disagree about the same file.
## Two things reviewers should know
**The oxlint JS plugin API is alpha.** oxlint itself is stable and
already this repo's frontend linter; the plugin API is the new
dependency. Its documented failure mode
([oxc#25203](https://github.com/oxc-project/oxc/issues/25203)) is being
skipped silently while oxlint still reports success. That affects the
standalone release binary rather than the npm package this invokes, but
the class of failure reads exactly like clean code, so the run asserts
`number_of_rules >= 1` from oxlint's own report and a broken engine
exits 2 rather than passing. If the API ever breaks, the fallback is
folding these rules into the line scanner, which already implements all
nine for Java and Python.
**`.claude/settings.json` is now committed**, carrying the hook and
nothing else: 19 lines, no `permissions`, nothing machine-specific. That
partly reverts `c35546a212` ("Ignore claude dir"), which existed because
this file had twice been committed by accident with a personal
`permissions` allowlist, once with absolute machine paths. Personal
config still belongs in `.claude/settings.local.json`, which the new
pattern keeps ignored, and hook entries merge across the two so nobody's
own hooks are lost.
If you already hand-wrote a `.claude/settings.json`, copy it somewhere
first: that path used to be git-ignored, and git overwrites an ignored
file without warning when a commit starts tracking it. Across 19 local
checkouts here, 13 have `settings.local.json` and none has a
hand-written `settings.json`.
To turn the hook off, `{ "env": { "COMMENT_LINT_HOOK": "0" } }` in local
settings. Claude Code can only disable all hooks at once, hence the
switch. The commit-time gate still applies.
## How to test
```bash
task pre-commit:comment-lint:ci
```
The fixture corpus, then the diff. The corpus checks the rules
themselves rather than the code under review, so it runs on CI and
before a rule change, not on every local commit.
```bash
task comment-lint:branch
```
`clean (34 files in scope)`. `task comment-lint` is the same thing
scoped to uncommitted work, which is what the git hook and CI run.
To watch it bite, add `// Is banner` above `export function isBanner` in
`scripts/lint/comment-rules.mjs` and run `task comment-lint`: one
`CMT001`, exit 1. The gate covers its own source, which is why these
scripts have no section dividers.
```bash
task pre-commit:comment-lint:all
```
The standing backlog, report-only.
Verified on the pinned oxlint 1.77.0, not only the 1.79 the plugin was
prototyped against.
# Description of Changes
When #6697 merged, the CI didn't run for some reason so it was never
caught that the tool models were out of date. This PR updates them to
the correct state.
# Description of Changes
Fixes various bugs that affected SaaS (and some self-hosted):
- Refreshing on Editor caused the user to be redirected to Processor
- User was unable to access Processor in SaaS
- Deep link hijacking fixes
- Fix double prefix `/app/app` issue
- Fix going from tool -> editor -> processor -> editor putting you back
into tool
---------
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
## Description of Changes
- Removed overlapping Gradle subdirectory entries from
.github/dependabot.yml.
- Dependabot now monitors the root Gradle project through /.
- Prevents duplicate pull requests for dependencies declared in Gradle
subprojects.
Closes: Not applicable
---
## Checklist
### General
- [ ] I have read the Contribution Guidelines
- [ ] I have read the Stirling-PDF Developer Guide (if applicable)
- [x] I have performed a self-review of my own code
- [ ] My changes generate no new warnings
### Documentation
- [ ] I have updated relevant documentation (if applicable)
- [ ] I have read the translation tag documentation (for new translation
tags only)
### UI Changes (if applicable)
- [ ] Screenshots or videos are attached
### Testing (if applicable)
- [ ] I have tested my changes locally
## What
The `promo` banner tone was a full-bleed `indigo-500 → purple-500`
gradient with white text and a black drop-shadow on the CTA. It was the
only saturated fill in the app, and against the warm neutral palette it
read as a foreign object above the workbench.
The bar is now app chrome:
| | Before | After |
|---|---|---|
| Background | 135° indigo→purple gradient | `--c-bg-raised` |
| Border | `transparent` | `--c-border-subtle` hairline |
| Icon | white glyph, no container | neutral glyph in a
`--c-surface-sunken` chip |
| Text | forced white | `--c-text` / `--c-text-muted` |
| CTA | `premium` accent (violet gradient) | `default` accent (same
primary button as the rest of the app) |
Before
<img width="1504" height="739" alt="Screenshot 2026-08-27 at 4 49 00 PM"
src="https://github.com/user-attachments/assets/a912d9e9-9590-4e1d-8202-1abb22a00f23"
/>
After
<img width="1061" height="665" alt="Screenshot 2026-08-27 at 4 48 30 PM"
src="https://github.com/user-attachments/assets/3be14aec-ccfe-4448-93b3-339a57be4937"
/>
Only caller is the friendly variant of `UpgradeBanner` (self-hosted,
under the free-tier user limit).
## Notes
- **No new theme tokens.** Every value is an existing `--c-*` semantic
token, so light and dark both follow automatically with no per-theme
overrides.
- The `premium` accent itself is untouched, so the upgrade CTAs in
`OfflineActivationCard` and `PairingPanel` are unaffected.
- `--c-hue-indigo` / `--c-hue-purple` are still used by
`SaaSOnboardingSlides`, `PaygFree` and `UpgradeModal`, so no tokens are
orphaned.
- Deleted comments describe rules that no longer exist (the gradient,
the white-on-gradient text overrides, the CTA shadow). No new comments
added.
## Verification
- `task frontend:check:all` passes (typecheck, oxlint, all four theme
linters, stylelint, format, tests, build, storybook build).
- `task frontend:storybook:a11y:changed` passes light and dark: 7
AppBanner stories, 0 violations. Both a11y baselines are empty, so this
is zero known violations rather than a baselined pass.
- Checked in Storybook under **Shared / AppBanner → All Top Bars**,
which renders every top bar the app can show side by side, in both
themes.
Our classification labels were rendering their hardcoded English names
because the en-US locale file had no `classification` section at all, so
this adds the missing keys (labels and category names).
Also wires the category names through i18n, since those had no `t()`
call, and adds a test so a new label can't ship without its key.
# Description of Changes
After ui rework all scrolling in all tool panels stopped working
This fixes this to allow tool panels to be scrollabe again
## What was wrong
PDF/UA is the only convert target whose settings panel overflows the
tool rail. Measured at 1920×1080: overflow was 0px for pdfa, pdfx, png,
docx, epub, and 158px for pdfua. Its action button sat at bottom: 1220
in a 1080px viewport — 140px below the fold — and the info alert was
clipped mid-sentence. The panel could be scrolled, but nothing said so
(Mantine's scrollbar auto-hides).
Normally the app would scroll the button into view for you. It didn't,
because both mechanisms built to do that were dead
## Cause:
Two separate mechanisms, both broken since the same commit (0a50e765b7,
frontend editor restructure, 2026-05-22):
1. ReviewToolStep - shared by all 47 tools. It looked for its scroll
container with:
stepRef.current.closest('[style*="overflow: auto"]')
Mantine's ScrollArea viewport sets inline overflow: scroll, not auto. I
measured it live - closest() returns null, and
document.querySelectorAll('[style*="overflow: auto"]') finds exactly 1
element anywhere in the page, and it isn't an ancestor of the panel. So
the lookup silently found nothing and the scrollTo never ran, for every
tool.
2. Convert.tsx - Convert only. It declared scrollContainerRef and a
scrollToBottom() wired to two useEffects, but the ref was never attached
to any element - createToolFlow() builds the JSX and no ref is passed
through. Always null, so both effects were no-ops.
Nothing else in the codebase has this pattern - I grepped for other
closest('[style*="overflow…"]') lookups and other
scrollToBottom/scrollContainerRef uses and both came back empty.
## The fix
createToolFlow.module.css (new) + createToolFlow.tsx:156 — the execute
button gets a position: sticky; bottom: 0 footer, the house pattern
already used by FormFill.module.css. Applied only when the review step
isn't visible, so it can never float over results. Sticky is inert when
content fits, so the other 46 tools are untouched.
ReviewToolStep.tsx:21 — real findScrollParent() walk replacing the
broken selector, scrolling by the minimum delta needed and only the
panel itself (never scrollIntoView(), which drags every ancestor). Also
added the missing clearTimeout cleanup.
Convert.tsx — deleted the dead ref and its two effects.
---
## 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.
Links a self-hosted instance to a SaaS team over an ordinary redirect,
and leaves the admin's browser holding a Stirling session at the same
time.
## The problem
A self-hosted server needs a device credential bound to a SaaS team, and
the admin's Supabase JWT must never reach the instance backend. Three
things ruled out the obvious approaches:
- **A customer hostname can never be in Supabase's redirect
allow-list**, so the sign-in cannot happen on the instance's own origin.
That is why SSO and sign-up did not work for linking at all.
- **A device credential identifies a server, not a person.** Every
attended portal read (Usage, Billing, Documents, Infrastructure) goes
through `getPortalSaasToken()` and needs a *user* session, so a
credential-only link left all of them asking for a second sign-in.
- **The previous design relayed a JWT** from the browser into the
instance, which is the thing we wanted to avoid. That path is deleted
here.
## The solution
Redirect and nonce, modelled on desktop's
`authService.loginWithSelfHostedOAuth`: mint a nonce, hand the browser
off, accept only a callback carrying that nonce back. Desktop has the OS
route the reply; self-hosted has no OS hop, so our own approval page
performs it. That is the point — the human half happens on an origin we
control.
```
instance SaaS admin's browser
| POST connect/request | |
| (name, callback, nonce, | |
| claim-secret hash) | |
|-------------------------->| |
| <- requestId + authorizeUrl |
| | GET /link?request=... |
| |<-------------------------------|
| | sign in (SSO works here), |
| | see ACCOUNT + ORIGIN, approve |
| |------------------------------->|
| | 302 callback#nonce+session |
| POST connect/claim | |
| (requestId, claim secret)| |
|-------------------------->| |
| <- device credential | |
```
Four properties carry the safety, and each is stated in the code because
each is easy to lose in a refactor:
- **The redirect target is never caller-supplied.** Validated once at
creation, then read back from the stored row, so nothing in the approval
page's URL can steer the token elsewhere.
- **Approval and minting are separate.** Approval records the team and
hands out nothing usable; the credential is minted only on claim,
authenticated by a secret that never entered a browser.
- **A re-authentication cannot move a server between teams.** The team
is pinned at creation from the credential only that instance holds, so
an approver from another team gets `WRONG_TEAM` instead of a rebind.
- **The approver has to confirm what they are binding.** The page shows
the address and the signed-in account, with a way to switch, and a
checkbox naming the address gates the approve button. The name the
server reports is deliberately not shown: the requester picks it on an
unauthenticated endpoint, and its honest value is the hostname already
in the address.
The session rides the URL fragment, so it stays out of access logs and
`Referer`, and is stripped before anything awaits. The claim is
row-locked, so one approval mints once. A request lives 30 minutes; a
settled one is not offered again, since approving it fails server-side.
Signing in mid-flow no longer loses the request. The id is kept on the
SaaS origin and resumed after any sign-in, which is what makes creating
an account work: the confirmation email opens a new tab, where the
`next` parameter is gone. Reading it does not consume it — the request
may be open in two tabs — and only a recorded decision retires it.
The result lands as a modal over the portal the admin started from, and
the portal re-reads its link status so the page behind agrees with the
modal.
Plaintext `http://` callbacks are accepted rather than refused, because
many self-hosted instances legitimately run plain HTTP on a private
network; the address carries a warning icon explaining the risk, derived
server-side so a requester cannot suppress it. Hard-refusing `http://`
to a public IP literal is a reasonable follow-up; a bare hostname can't
be classified without a DNS lookup, so the warning stays the general
mechanism.
## Configuration
Four surfaces. Placeholders below, not values.
**SaaS backend**
| Setting | Needed | Why |
|---|---|---|
| `stirling.billing.account-link.enabled` | Yes, `true` | The connect
controller and service are `@ConditionalOnProperty` with no default, so
without it the endpoints do not exist. |
| `system.frontendUrl` | Only when the approval page is not on the API's
own origin | Where the approver is sent. Must include the app's base
path if it is served under one, or the redirect misses `/link`. |
**SaaS frontend**
| Setting | Needed | Why |
|---|---|---|
| `VITE_SUPABASE_URL`, `VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY` | Yes |
Its own sign-in. Must be the project the SaaS backend validates tokens
against. |
| `RUN_SUBPATH` | Only if served under a subpath | Moves the approval
page to `<base>/<subpath>/link`, so `system.frontendUrl` has to agree. |
**Self-hosted backend**
| Setting | Needed | Why |
|---|---|---|
| `stirling.billing.account-link.enabled` | Yes, `true` | Defaults to
`false`. |
| `stirling.billing.account-link.saas-base-url` | Yes | Origin of the
SaaS API it links to. Not the SaaS frontend. |
| `system.frontendUrl` | Optional | Externally reachable base URL for
the callback. Otherwise derived from the request's `Origin`, which is
right for ordinary deployments and wrong behind a rewriting proxy. |
**Self-hosted frontend**
| Setting | Needed | Why |
|---|---|---|
| `VITE_SUPABASE_URL`, `VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY` | Yes |
Accepts the session handed over in the callback fragment. |
| `VITE_SAAS_API_URL` | For Usage and Billing | Attended reads go to the
SaaS API with the admin's token. Absent, those surfaces stay on the
mock. |
| `VITE_INCLUDE_PORTAL` | Production builds | Dev builds include the
portal automatically; without it there is no link UI and no callback
route. |
Two things worth stating because neither fails loudly:
- **Both frontends must use the URL *and* key of the same Supabase
project**, and the same one the SaaS backend validates against. A key
from one project with a URL from another is accepted by the browser and
rejected by Supabase, which surfaces much later as "session expired" on
Usage rather than as an error at hand-over.
- **The Supabase redirect allow-list must contain the SaaS app's
`/auth/callback`**, since a confirmation email returns through it.
Entries are matched exactly.
- **`system.frontendUrl` is the existing setting for this**, not a new
one, so each side reads its own value and there is nothing extra to
configure. It also gates share links, so on a stack with storage and
sharing already on, setting it here turns those on too.
The self-hosted side deliberately does **not** configure where the
approval page lives — SaaS answers that in the connect-request reply,
being the only party that knows.
Also here, because testing this needs two stacks side by side:
`linked:staging` / `linked:dev` (which derive `system.frontendUrl` and
`RUN_SUBPATH` themselves), the missing `frontend:staging:saas`, and a
per-mode vite `cacheDir` — two dev servers in different modes otherwise
re-optimise over one shared dep cache.
## How to test
Automated and green: `task frontend:check:all` plus both backend
modules. `ConnectRequestServiceTest` covers callback validation, the
per-IP cap, single-use approval, claim outcomes, expiry, `WRONG_TEAM`
and reauth confirming without minting; `ConnectServiceTest` covers
callback-resolution precedence including a foreign-origin callback being
discarded; `ConnectControllerTest` covers the authorize URL, including
the forwarded-header path and only the first hop being trusted;
`ConnectCallback.test.tsx` covers the fragment being stripped
synchronously and malformed fragments refused;
`LinkAccountModal.test.tsx` covers link and reauth hitting different
endpoints.
Manual walkthrough:
1. `task linked:staging` — added here; brings up a SaaS stack and a
self-hosted instance pointed at it, on discovered ports, and prints the
four addresses.
2. Open the link-account modal in the self-hosted portal and continue.
Expect the SaaS approval page at `/link?request=<id>`.
3. Sign in as a team leader, or create an account and confirm the email.
Either way you should come back to the approval page.
4. Tick the acknowledgement and approve. Expect the fragment gone from
the address bar immediately, a result modal over the portal, the portal
showing linked without a reload, and attended reads (Usage, Billing)
working without a second sign-in.
5. Repeat, approving as a member of a different team. Expect a refusal,
not a rebind.
## Outstanding
- #7415 to be reworked against this design once this lands.
- **No SaaS-side UI to disconnect a server.** `GET
/account-link/instances` and `POST /account-link/instances/{id}/revoke`
are already team-scoped and leader-gated, and the portal has a panel
that uses them, but
`portal-saas/components/settings/accountLinkSettings.tsx` exports `null`
on the reasoning that "SaaS has no account-link concept". That held when
linking was a self-hosted admin managing their own instance; here a
leader approves a server they may not administer, and has no way to
withdraw it. The seam to fill is that one file. Expected to land with
the CTA work in #7415.
---------
Co-authored-by: James Brunton <jbrunton96@gmail.com>
## What
Switching editor -> processor (or reloading) unmounts every editor
provider, which emptied the workbench. This PR mirrors the workbench
into per-tab sessionStorage and refills an empty one from that record on
the next mount:
- **Files, selection, view and active document survive** the shell
switch and reloads. Each recorded file is resolved to its *current leaf*
version on restore, so a file versioned by a policy or another tab comes
back at its latest state.
- **The switch back lands where the user left**: the processor sidebar's
"editor" button consumes a one-shot return path saved at switch time.
- **The app switch respects unsaved changes**: `useOtherAppSwitch`
(proprietary + saas) now routes through `requestNavigation`, so the same
warning guards it as any other navigation.
- Desktop shadows `WorkbenchSessionPersistence` with a stub (OS-launched
files own boot there).
## How to test
I've run through each of these manually:
- Upload several PDFs in the editor, select a couple, and switch to the
Active Files grid. Click "Open PDF Processor" in the sidebar footer,
then switch back to the editor. The same files, selection and view
should return, and you should land on the editor page you left.
- Open a document in the viewer, then reload the tab. The workbench
should refill and come back on the viewer with the same document active.
- With unsaved changes in a tool, click the processor switch. The
unsaved-changes warning should appear, and the switch should only
proceed if you confirm.
- Open a second browser tab with different files. Each tab should
restore its own workbench independently (the record is per-tab
sessionStorage).
- While in the processor, delete one of the open files from storage,
then switch back. The remaining files should restore and a warning toast
should report "Restored X of Y files".
---------
Co-authored-by: Claude <noreply@anthropic.com>
Follow-up to #7580: the escalation it added could never fire.
## What's broken
The auto-run skips a policy that has already run on a file, keyed on
`(categoryId, fileId)`. `recordRunStart` claims that key — and #7580 has
the **browser-side first pass** record its own run under `categoryId:
"classification"` for the uploaded file. So the local heuristic ticks
the very key the server escalation checks, and the AI is never asked, at
any confidence.
Trigger is the default seeded setup: **Classification as the only
on-upload policy**, and a local verdict below `high`. Any other
on-upload policy masks it, because classification then targets that
policy's output — a new file id whose key was never claimed. That's why
this went unnoticed.
Two smaller faults in the same path:
- A chained output carried no `classificationConfidence`, so
`shouldDispatchToAi` waited for a verdict that could never arrive (a
tool-derived file gets no local pass).
- Browser-local runs were polled against the server: 3 × 404 per file,
after which `MAX_NOT_FOUND` marked a local run that had actually
**succeeded** as `FAILED`.
## The fix
- `PolicyRunRecord.browserLocal`; `recordRunStart` skips the dispatch
claim for such a run. It is the first pass, not the policy's run.
- The local pass meters under `classification:local-meter` instead of
the category id, so metering dedupe survives without suppressing
dispatch.
- The poll effect skips browser-local runs.
- `CONSUME_FILES` inherits `classificationConfidence` alongside the
labels, so the verdict survives a version bump.
## How to test
Download
[`low-confidence-classification.pdf`](https://github.com/Stirling-Tools/Stirling-PDF/raw/fix/chained-classification-confidence/frontend/editor/src/proprietary/services/heuristic/fixtures/low-confidence-classification.pdf)
(checked in as a fixture, verdict pinned by a test).
With **Classification as the only on-upload policy**, upload it and
watch the Network tab:
- **Before:** no `POST /api/v1/policies/{id}/run` for classification,
ever. Console shows `local-classification-*` 404s.
- **After:** exactly one, and the engine receives `POST
/api/v1/documents/classify`.
Judge it on that request, not on the resulting label — the model's
answer varies, so a label comparison can pass or fail for the wrong
reason.
Headless equivalent:
```
npx vitest run --project proprietary src/proprietary/components/policies/usePolicyAutoRun.escalation.test.tsx
```
Passes here, fails on `main` on "asks the AI about an unsure verdict
even though the local pass already ran". Its other two cases pass on
both, so the guards still hold: a confident verdict still costs nothing,
and a file with no verdict yet still waits rather than racing the free
pass.
New tests drive the **real** run store — mocking it is what let this
through.
`task frontend:check`: 255 files / 2202 tests.
# Description of Changes
Originally, I wanted to re-enable typed linting on our repo but using
Oxlint this time to avoid the memory and speed issues that ESLint was
causing. Unfortunately, it's not stable enough yet to actually use on
our repo (although it is close, I suspect it'll be stable enough fairly
soon). I was able to remove many of the unnecessary casts that it found
though, so even though this won't be enforced, it's still worth cleaning
up what I've found.
The bundled signing test certificates expired at **07:41:10 UTC on
2026-08-26**. They were issued exactly one year earlier, so they went
from fine to fatal mid-morning with no warning, and they take down
`main` and every open branch, not just one PR.
First casualty was the `docker-compose-tests` job on #6802, which
started at 07:45:
```
java.security.cert.CertificateExpiredException: NotAfter: Wed Aug 26 07:41:10 UTC 2026
at CreateSignatureBase.checkValidity(CreateSignatureBase.java:159)
at CertSignControllerTest.testSignPdfWithPkcs12(CertSignControllerTest.java:205)
```
```
$ openssl x509 -in app/core/src/test/resources/certs/test-cert.pem -noout -dates
notBefore=Aug 26 07:41:10 2025 GMT
notAfter =Aug 26 07:41:10 2026 GMT
```
## What was broken
`CertSignControllerTest` (7 tests) and `PdfSigningServiceImplTest` (2)
fail outright. `ValidateSignatureControllerMoreTest` and
`CertificateValidationServiceMoreTest` read the same fixtures.
Auditing the rest of the repo turned up three more time bombs that had
not gone off yet:
| Fixture | Was | Problem |
|---|---|---|
| `app/core/.../certs/test-cert.*` + `test-key.*` | expired 2026-08-26 |
**already breaking every branch** |
| `test-certs/valid-test.p12`, `valid-test.jks` (proprietary + frontend
copies) | expire 2027-03-25 | same failure, seven months out |
| `test-certs/not-yet-valid-test.p12` | valid **from** 2027-03-25 |
becomes valid, so its test silently stops proving anything, on the same
day |
## What this does
**Regenerates every fixture** with the identical subject DN, alias,
password, key size and signature algorithm as before, changing only the
validity window. Nothing that any test asserts on has moved.
- valid fixtures: `2025-01-01` to `2125-01-01`
- `not-yet-valid-test.p12`: `2125-01-01` to `2126-01-01`, so it stays in
the future
- `expired-test.p12`: pinned to its permanently-past 2024 window
**Adds `scripts/generate-test-certs.sh`** as the source of truth, so the
next regeneration is one command instead of archaeology. It documents
every DN, alias and password, pins the validity windows, and runs on
Linux, macOS and Git Bash.
**Adds two guard tests** that fail with an actionable message, naming
the script, while there is still a year of runway:
- `BundledTestCertificateExpiryTest` (app/core) checks all seven formats
parse, are in their validity window, and have more than 365 days left
- `BundledWorkflowCertificateExpiryTest` (proprietary) does the same for
the valid pair, and additionally asserts the expired fixture is still
expired and the not-yet-valid one is still in the future
That last pair matters: those two fixtures exist to test a validity
outcome, and each one silently stops testing anything once the clock
passes its window.
## Verification
Run locally against the regenerated bytes, on the exact content
committed here:
```
./gradlew :stirling-pdf:test --tests '*CertSignControllerTest*' --tests '*BundledTestCertificateExpiryTest*' \
--tests '*PdfSigningServiceImplTest*' --tests '*ValidateSignatureControllerMoreTest*' \
--tests '*CertificateValidationServiceMoreTest*'
BUILD SUCCESSFUL
./gradlew :proprietary:test --tests '*BundledWorkflowCertificateExpiryTest*' --tests '*CertificateValidationIntegrationTest*' \
--tests '*SigningFinalizationServiceMoreTest*' --tests '*ServerCertificateServiceTest*' \
--tests '*CertificateSubmissionValidatorTest*' --tests '*WorkflowSessionServiceTest*'
BUILD SUCCESSFUL
```
`spotlessCheck` passes on both modules.
# Description of Changes
e2e Playwright tests are currently failing intermittently on all
platforms for different reasons, most notably WebKit, which seems to
fail much more often than the others. This PR attempts to fix the
issues. I've ran the e2e tests a few times now and they don't seem to be
inconsistent any more, but it's difficult to tell if all the issues are
genuinely fixed due to the inconsistent nature. As far as I can tell,
I've not broken anything though.
Review Flow PR 4. Stacked on #7477. Recorded failures appear in a
notification bell, showing each reader the failures they are allowed to
see and the actions they can actually take.
Scope is deliberately viewing and routing only. Resolving a failure —
retry, decrypt-and-retry — is #7479, which also brings the write path
for it; nothing resolution-shaped ships here, not even dark.
## What's added
**A notification bell** in the editor and the processor shell. Polls
`GET /api/v1/notifications` every 30 seconds, shows an unread badge, and
lists open failures newest first. Each row shows the failure's title,
its message with **Copy error** and **Show full message** chips, an
occurrence count, and its available actions.
**A notification API** (`stirling.software.proprietary.notification`),
derived from failures on read rather than stored in its own table:
| Route | Purpose |
|---|---|
| `GET /api/v1/notifications` | the caller's open failures, newest first
|
Read-only by design: every action the bell offers is one the client runs
on its own device, so there is nothing to post back. Every id is
prefixed (`failure:<uuid>`), so the bell never holds a raw failure id it
could hand to a failure endpoint.
**Per-reader actions.** A `FailureKind` declares each action with an
audience (`OWNER`, `TEAM_REVIEWER`, `ANYONE_WHO_SEES`). The server
resolves that against the reader and derives `Ownership` (`MINE` /
`THEIRS` / `UNOWNED`) from the row's actor, so an admin reviewing
someone else's failure is not offered a document their browser does not
hold. Adding a failure kind requires no frontend change.
**Server-run and client-run actions are distinguished.**
`FailureActionId` carries an `Execution` facet; the registry requires a
bean only for server actions, and dispatching a client action on the
failure surface returns 400. The notification projection goes further:
it carries only client-run offers, so the bell cannot be sent a button
it would refuse to draw.
**Actions in the bell:** at most two. The owner of the document gets
**View file** (opens it in the editor); a team reviewer gets **View in
processor** (dev builds only). Dismiss stays on the failure queue in
`/processor/documents` — deciding a failure's fate belongs to the review
surface, not the panel that announces it. An action id the build has not
wired is skipped rather than rendered dead, so the server can ship new
kinds ahead of the clients that understand them.
**Attended policy runs record their document.** `POST
/api/v1/policies/{id}/run` accepts an optional opaque `fileId`, recorded
when the run carries exactly one primary document. This is what lets a
repeat fold onto one incident instead of opening a new one per upload,
lets deleting the file clear its failure, and lets the owner open the
document from the row.
## Behaviour changes
- **The bell re-reads as soon as a failure you caused is recorded**,
rather than leaving you to wait out a poll interval for news of your own
upload. Applies to a failed tool run and to a policy run reaching
`FAILED`. Other people's failures still arrive on the poll, which is
what it is for.
- **An action the reader cannot use is not rendered.** Where the server
gave a reason for withholding it, that reason appears as the row's
one-line note. An action that was never offered to that reader produces
no note.
- **Deleting a document closes every incident about it that the deleter
caused**, including a failed policy run on their own upload, so a user's
own errors leave the bell with the file rather than lingering with a
dead button.
- **The failures list in `/processor/documents` stays behind
`import.meta.env.DEV`**, and View in processor is gated to match so it
cannot navigate to a section that is not mounted. Both lift when
failures get their own review screen.
- **One poll for all bells.** The bell is mounted in three places; the
list, document lookups and read marker are shared, so mounting more than
one does not multiply requests.
- `ACKNOWLEDGE` is no longer offered by any kind. The id, bean and
status remain so existing rows stay readable.
## Known limits
- The poll does not pause when the tab is hidden.
- No retention or per-team cap on `file_run_events`.
## How to test
Needs a proprietary or SaaS build with login enabled. `task dev:all`,
then sign in.
1. **Create a failure.** Add a password-protected PDF to the editor and
choose **Skip for now** when it asks to unlock. The upload starts a
policy run that fails on it.
2. **Watch the bell.** The badge should appear within a second or two,
not after 30 — this is the refresh-on-failure path. Open it: a row
titled "Password-protected document" with the error message and the two
chips.
3. **The buttons should be View file and View in processor, nothing
else.** No Dismiss and no retries: dispositions live on the review
surface, resolutions in #7479.
4. **View file** closes the panel and selects that document in the
editor.
5. **Dismiss from the queue instead.** Open `/processor/documents` (dev
build), find the row in the failures list and dismiss it there; the bell
drops it on its next read.
6. **Confirm the local-document probe.** Create a second failure, then
delete that file from the editor and reload. Its incident closes with
it; a row whose document is still present keeps **View file**.
7. **Confirm attribution end to end.** Sign in as a plain member, run a
shared policy on your own upload so it fails. The member sees their own
row in the bell. Sign in as the team leader: they see it too, but with
**View in processor** instead of **View file**, because the document is
not in their browser.
8. **Confirm folding.** Add the same locked PDF again and skip again.
The existing row's occurrence count increases rather than a second row
appearing.
9. **Confirm one poll for many bells.** Open the editor and the
processor in two tabs. Each tab issues its own poll, but within a tab
the several mounted bells share one — the Network tab should show one
`GET /api/v1/notifications` per 30s per tab, not three.
## Migration
None. No new column and no new value in any CHECK-constrained enum;
`CheckConstrainedEnumsTest` fails if that changes.
Auto-generated by stirlingbot[bot]
This PR updates the frontend license report based on changes to
package.json dependencies.
Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
Auto-generated by stirlingbot[bot]
This PR updates the backend license report based on dependency changes.
Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
# Description of Changes
Building ontop of a users draft PR for form creation tools
**Fill Form** becomes a full **Form Editor**: fill, create, modify and
delete AcroForm fields visually. Builds on the community form-creation
draft, plus a UX/UI rework pass.
- **Backend**: `/api/v1/form` endpoints — `fields-with-coordinates`,
`add/modify/delete-fields`, combined `edit-fields` (one round-trip),
`fill`, `extract-csv/xlsx`; supports text (multiline, comb), checkbox,
dropdown, list box, radio, button actions (reset/print/URL/submit) and
signature placeholders
- **Create**: type palette, click-or-drag placement with snap guides,
inline property editor, batch "Add N fields"
- **Modify**: move/resize on the page, arrow-nudge + Delete key, X/Y/W/H
inputs, staged edits/deletes with chips, discard
- **Fill**: live progress + required tracking, flatten toggle, Export
menu (JSON/CSV/XLSX), Ctrl/Cmd+S
- **Safety**: confirm dialog before discarding staged work; empty
required fields warn with "Save anyway" instead of blocking
- **UI**: consistent panel skeleton (fixed header / scrolling list /
pinned actions), empty states that link into Create, full i18n with
plural keys
[walkthrough.html](https://github.com/user-attachments/files/30508976/walkthrough.html)
---
## 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.
---------
Co-authored-by: Denys Vitali <denys@denv.it>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
# Description of Changes
The Documents tab in the Processor is supposed to be available to all
Processor users, but because the API is built on top of the Audit data,
which is only for enterprise users, the API call always fails with 403.
This means that it never fills the query cache, so every time you go
back to the tab it has to reload all the data for a couple of seconds
(and will fail again). This fixes the API so that it's available to any
Processor user instead of just enterprise users. Also, the documents
data was only being written to the log on an enterprise license, so I've
changed it so that data is always tracked in the audit log because
otherwise the Documents tab would still be useless to non-enterprise
users.
The Audit Log tab was also available to all Processor users, but would
have the same issue where the table would never load because the API
would 403 as well. I've just made the Audit Log tab disabled for
non-enterprise users now. We might want to do something to signpost it a
bit more that it's an enterprise-specific feature, but it's better than
nothing for now.
Split out of #7574 — this is the classification half, which is
independent of the editor-source work and can land on its own.
## What this does
- **Runs the local heuristic first and only escalates an unsure verdict
to the AI.** A high-confidence local answer stands; anything less (or a
file the heuristic hasn't reached yet) goes to the engine. A wrong label
costs more than an engine call, so the bar is deliberately strict.
- **Makes `classify` an authorable pipeline task**, so it can be used as
a step like any other tool, and skips files that are already classified.
- **Leaves the seeded Classification policy unowned** rather than naming
a `system` placeholder that was never a real user; existing seeds are
repaired on boot.
## Review feedback applied
From @jbrunton96 on #7574:
- **The generic runner no longer names classification.** Everything
classification-specific moved into
`proprietary/data/classificationPolicy.ts`, and `usePolicyAutoRun` now
asks capability questions instead: `policyRewritesDocument`,
`policyDeliversOutputFiles`, `policyRequiresAiEngine`,
`shouldDispatchToAi`. There is no `id === "classification"` left in the
runner.
- **Ordering is no longer a name in the runner.**
`pinClassificationLast` is gone; the runner sorts annotating policies
after rewriting ones. The constraint is real: an annotating policy is
non-blocking, so a rewriting one running after it forks from the
pre-annotation version and drops the labels. To be straight about what
this is and isn't - see "Still open" below - `policyRewritesDocument` is
still keyed on the category id, not on a property each policy declares.
The check moved out of the runner; it did not stop being a check on one
id.
- **Confidence is typed.** New `ClassificationConfidence` union in
`core/types/fileContext.ts`, reused by `fileStorage`,
`HeuristicConfidence`, and the trusted-verdict constant instead of being
respelled at each site.
- **Comments trimmed** to the repo's 2-line guideline, and a stale
seeder javadoc that still claimed an internal-user owner was corrected.
## Still open, deliberately
`classificationPolicy.ts` answers its capability questions with
`categoryId === "classification"`. That is the same check relocated, not
removed, and the module doc now says so outright.
Deliberate, for two reasons:
- **The concept it would be declared against is going away.** Policies
are becoming pipelines with labels behind a separate enforcement layer,
which removes the category the flag would live on. A capability system
built on `categoryId` today gets migrated twice.
- **Classification is genuinely privileged, not accidentally special.**
It is the only policy with a browser-side implementation, so it can
answer without the server. That is a product decision, and a local-only
mode for set scenarios is planned - the flag for it should be designed
with that feature, not guessed at now.
The end state for the rest: an in-place output mode retires the ordering
rule and `policyDeliversOutputFiles`, and a run result that can carry
findings as well as files retires the remainder. Both touch the import
path, which is the most delicate code in `usePolicyAutoRun` - not
something to bolt on to a PR that has already been split once.
Nothing is broken by leaving it. A user-built classify pipeline still
gets its labels: the generic import path reads them off the returned
PDF. It versions the file instead of labelling in place, and it misses
the local-heuristic shortcut, so it always bills the engine.
## Testing
- `classificationPolicy.test.ts` — 12 cases covering each capability and
the escalation rule
- Full frontend `proprietary` project: 39 files / 442 tests
- `:proprietary:test` for `DefaultClassificationPolicySeederTest` +
`ClassifyLabelControllerTest`
- `tsc --noEmit` on core, proprietary, portal, saas, desktop, cloud
---------
Co-authored-by: James Brunton <jbrunton96@gmail.com>
Bumps org.snakeyaml:snakeyaml-engine from 3.0.1 to 3.1.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>
# Description of Changes
Prettier takes about 10 seconds to run over our frontend folder, but
[Oxfmt](https://oxc.rs/docs/guide/usage/formatter.html) does an (almost)
identical job in 0.2 seconds. This PR converts our Prettier integration
to an equivalent Oxfmt integration. There's exactly 2 files in the
frontend folder that Oxfmt formats differently to Prettier so it'll
barely cause any disruption to the source.
I've removed the `--check` option from the frontend tool models
generator as part of this because we can do the same thing with Task
easily enough and Oxfmt isn't directly importable like Prettier since
it's a Rust binary instead of a JS library. Originally I was shelling
out to Oxfmt on single-file mode to keep it all in memory but it just
seemed more likely that there'd be config mismatches between that script
and the task so I think it's better this way.
# Description of Changes
Follow-on from #7334. Fix more `any` type usages and ban them in the
linter. We're starting to get down to only difficult folders left now,
so some of these fixes replace an excluded folder with a couple of
individual files to reduce scope to manageable levels.
There are two real behaviour changes in this PR because of bugs that
were never caught due to the lack of proper typing:
- In the Google Drive service, `lastModified` was always `undefined`
because it should have been read via `lastModifiedUtc`, which it now is.
This means that files being read from Google Drive should now accurately
retain their last modified date from Drive.
- In the error toasts, there was translation logic to try and make
friendlier error messages, but it'd never actually fire since it relied
on `i18n` being written to `globalThis`, which it never was. It now
imports the singleton instead so that translation should start working.
I also had to tweak the way that FitText works because it was relying on
`any` typing to mix refs between different places where they weren't
technically compatible but I've changed it to go via a function and the
behaviour doesn't change.
# Description of Changes
Follow-up to #7518, picking up two mobile rough edges found while going
over that branch. Two changes, one commit each.
## 1. Tool search back in the tool list (mobile)
Tool search lives in the workbench bar's super search, which on mobile
sits on the Workspace slide. So searching for a tool meant swiping off
the tool list, typing, then swiping back. This puts a filter at the head
of the tool panel on mobile. Reuses the existing `ToolSearch` component
in `mode="filter"`, the same one the desktop fullscreen picker uses.
Drives `setSearchQuery` on `ToolWorkflowContext`, so the query,
filtering and grouped results are all existing paths. `ToolPanel` takes
a new `showSearch` prop; `RightSidebar` passes `showSearch={isMobile}`.
Desktop renders exactly as before.
**To test:**
- Open the editor at a phone-width viewport (under 1024px).
- A "Search tools..." field should sit above Favourites / Recommended in
the Tools pane.
- Typing filters into grouped results. Clearing goes back to the compact
list.
- It hides once a tool is open, and comes back on the way out.
- On desktop the field should not appear at all.
## 2. The mobile overflow menu opened with nothing in it
`WorkbenchBarMobileActions` rendered its kebab trigger unconditionally.
But every item inside is gated on `currentView === "viewer"` or
`!isCustomView`. In a `custom:*` workbench both are false, so the
dropdown was empty. `WorkbenchBarDesktopActions` renders nothing in that
case, so this only showed on phones. Now returns `null` when neither
group applies, with the two conditions named so the trigger and the
items can't drift apart again.
**To test:**
- Phone-width viewport, load a PDF.
- Open a tool with its own workbench view: Compare, Get Info report,
Show JS, Validate Signature, Edit Table of Contents, or PDF Text Editor.
- The kebab at the right of the workbench bar should be gone entirely,
rather than opening an empty menu.
- Back in the viewer or page editor it should still be there, with Print
/ Download / Save As / Close.
Bumps `logback` from 1.6.1 to 1.6.3.
Updates `ch.qos.logback:logback-core` from 1.6.1 to 1.6.3
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/qos-ch/logback/releases">ch.qos.logback:logback-core's
releases</a>.</em></p>
<blockquote>
<h2>Logback 1.6.3</h2>
<h1>2026-08-14 Release of logback version 1.6.3</h1>
<ul>
<li>
<p>In response <a
href="https://www.cve.org/cverecord?id=CVE-2026-19880">CVE-2026-19880</a>,
<code>MDCBasedDiscriminator</code> (used by
<code>SiftingAppender</code>) now strips forward and backward slashes
(<code>/</code>, <code>\</code>) from MDC values before they are used as
discriminating keys. This prevents path segments from escaping into
destinations controlled by an attacker. When sanitisation actually
changes a value, a warning is emitted; the warning is rate-limited (a
small batch, then a lull of about ten minutes).</p>
</li>
<li>
<p>Colour console support is split out into a dedicated <a
href="https://logback.qos.ch/manual/appenders.html#JansiConsoleAppender"><code>JansiConsoleAppender</code></a>.
It wraps stdout or stderr with Jansi so ANSI escape sequences (for
example coloured patterns) render correctly on terminals that need it,
notably Windows. Prefer this class over the older path described next.
See the <a
href="https://logback.qos.ch/manual/appenders.html#JansiConsoleAppender">appenders
documentation</a>.</p>
</li>
<li>
<p>The <code>withJansi</code> property on <code>ConsoleAppender</code>
is <strong>deprecated</strong>. Existing configurations that still set
<code><withJansi>true</withJansi></code> continue to work
for compatibility, but new setups should use
<code>JansiConsoleAppender</code> instead.</p>
</li>
<li>
<p><code>ConsoleAppender</code> no longer treats the process console as
an exclusive resource: stopping it does not close
<code>System.out</code> / <code>System.err</code>.
<code>JansiConsoleAppender</code> pairs each
<code>AnsiConsole.systemInstall()</code> with
<code>systemUninstall()</code> on stop, so repeated start/stop cycles do
not leave Jansi installed or tear down streams shared with the rest of
the JVM. Related behavior is covered by tests for <a
href="https://redirect.github.com/qos-ch/logback/issues/1063">issues/1063</a>.</p>
</li>
<li>
<p>Invocation throttling helpers were reworked:
<code>SimpleInvocationGate</code> is renamed
<code>FixedIntervalInvocationGate</code>, and
<code>BatchedFixedIntervalInvocationGate</code> allows a short burst of
invocations before applying a fixed lull. The sanitisation
warning above uses the batched gate.</p>
</li>
<li>
<p>The JPMS <code>module-info</code> for logback-core now exports the
<code>ch.qos.logback.core.property</code> package, which had been
missing from the module descriptor.</p>
</li>
<li>
<p>A bit-wise identical binary of this version can be reproduced by
building from <a href="https://github.com/qos-ch/logback">source
code</a> at commit <code>e8e824dede022a6d7208b36cfa875b0d1b7772f3</code>
associated with the tag <code>v_1.6.3</code>. The release was built
using Java "21" 2023-10-17 LTS build 21.0.1.+12-LTS-29 under
Linux Debian 11.6.</p>
</li>
</ul>
<p>--
Sponsoring SLF4J/logback/reload4j at <a
href="https://github.com/sponsors/qos-ch">https://github.com/sponsors/qos-ch</a></p>
<h2>Logback 1.6.2</h2>
<p><a
href="https://github.com/user-attachments/assets/9ceaf157-b758-4188-815d-edfe4e1b4edd">https://github.com/user-attachments/assets/9ceaf157-b758-4188-815d-edfe4e1b4edd</a></p>
<h1>2026-08-10 Release of logback version 1.6.2</h1>
<ul>
<li>
<p>Configuration analysis now detects <em>contradictory caller-data
inclusion instructions</em>. For example, an <code>AsyncAppender</code>,
<code>SocketAppender</code> or <code>SMTPAppender</code> with
<code>includeCallerData</code> left at the default <code>false</code> is
incompatible with a layout or encoder pattern that uses a caller-data
converter such as <code>%C</code>, <code>%M</code>, <code>%L</code>,
<code>%F</code>, <code>%l</code> or <code>%caller</code>. At runtime
those converters would print question marks and still incur extraction
cost on a worker thread. Logback now emits a configuration-time warning
when such instructions disagree. See <a
href="https://logback.qos.ch/codes.html#callerContradiction">codes.html#callerContradiction</a>
for details. This issue was reported in <a
href="https://redirect.github.com/qos-ch/logback/issues/1059">issues/1059</a>
by <a href="https://github.com/leeychee">leeychee</a>. The initial
analysis was contributed by <a
href="https://github.com/seonwooj0810">seonwoo_jung</a>.</p>
</li>
<li>
<p>Caller-contradiction analysis can be turned off by setting the
<code>logback.skipCallerContradictionAnalysis</code> variable to
<code>true</code>, either as a system property
(<code>-Dlogback.skipCallerContradictionAnalysis=true</code>) or as a
property in the configuration file:</p>
<pre lang="xml"><code><property
name="logback.skipCallerContradictionAnalysis"
value="true"/>
</code></pre>
</li>
<li>
<p><code>SimpleSocketServer</code> and
<code>SimpleSSLSocketServer</code> now require an explicit client IP
whitelist. On the command line, pass one or more allowed addresses
(single IPs or CIDR ranges) after the configuration file. An empty
whitelist means no clients are accepted. When embedding the server
programmatically, register allowed addresses with
<code>addAllowedClientAddress(String)</code> or
<code>setAllowedClientAddresses(Collection)</code> before clients
connect. See the documentation on <a
href="https://logback.qos.ch/manual/appenders.html#simpleSocketServerClientAccess">restricting
client access</a>.</p>
</li>
<li>
<p>Added <code>ThrowableProxyVOBuilder</code> for assembling a
<code>ThrowableProxyVO</code> field by field, with a corresponding
<code>ThrowableProxyVO.builder()</code> entry point.</p>
</li>
<li>
<p>Dependency analysis handlers now run their <code>postHandle</code>
method after child models have been processed, so checks that depend on
nested appenders (such as caller-contradiction analysis) see a complete
picture.</p>
</li>
<li>
<p>Updated several dependencies, including Angus Mail to 2.0.4 and Jetty
(test) to 12.1.12.</p>
</li>
<li>
<p>A bit-wise identical binary of this version can be reproduced by
building from <a href="https://github.com/qos-ch/logback">source
code</a> at commit e3d78330ad1ba024fd987fd00c3ffb9cfcdb07dc associated
with the tag <code>v_1.6.2</code>. The release was built using Java
"21" 2023-10-17 LTS build 21.0.1.+12-LTS-29 under Linux Debian
11.6.</p>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/qos-ch/logback/commit/e8e824dede022a6d7208b36cfa875b0d1b7772f3"><code>e8e824d</code></a>
prepare release 1.6.3</li>
<li><a
href="https://github.com/qos-ch/logback/commit/761821bfaacac3a0ad44fa546cfc814429bf9312"><code>761821b</code></a>
MDCBasedDiscriminator has a gated warning mechanism</li>
<li><a
href="https://github.com/qos-ch/logback/commit/53ed1229008d8b1902f5c234deaa07d742890879"><code>53ed122</code></a>
update copyright year</li>
<li><a
href="https://github.com/qos-ch/logback/commit/c7e2db244671ffa916182b5da8c89579eb54a645"><code>c7e2db2</code></a>
rename SimpleInvocationGate as FixedIntervalInvocationGate</li>
<li><a
href="https://github.com/qos-ch/logback/commit/b5aa931b096a4b0b6a9e140b74fabe7da152cbf0"><code>b5aa931</code></a>
added BatchedSimpleInvocationGate</li>
<li><a
href="https://github.com/qos-ch/logback/commit/1f22af7686aadd25c08b4bd1e6943a906a743ad4"><code>1f22af7</code></a>
add javadocs to SimpleInvocationGate</li>
<li><a
href="https://github.com/qos-ch/logback/commit/638ffa7e7852478b605a91b3e91238ff26f8158c"><code>638ffa7</code></a>
prevent forward and backward slashes to escape to other directories</li>
<li><a
href="https://github.com/qos-ch/logback/commit/7d6b9a4f8c8996834c0a694f6c141705a003d7bb"><code>7d6b9a4</code></a>
add missing ch.qos.logback.core.property package</li>
<li><a
href="https://github.com/qos-ch/logback/commit/fa25930346f35636fb6a077c1f66ebb06edd3b6f"><code>fa25930</code></a>
add an extension path in ConsoleAppender for JansiConsoleAppender</li>
<li><a
href="https://github.com/qos-ch/logback/commit/c73b43f2011f9d4545abc7ea461172276a0a43b3"><code>c73b43f</code></a>
deprecate the withJansi path</li>
<li>Additional commits viewable in <a
href="https://github.com/qos-ch/logback/compare/v_1.6.1...v_1.6.3">compare
view</a></li>
</ul>
</details>
<br />
Updates `ch.qos.logback:logback-classic` from 1.6.1 to 1.6.3
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/qos-ch/logback/releases">ch.qos.logback:logback-classic's
releases</a>.</em></p>
<blockquote>
<h2>Logback 1.6.3</h2>
<h1>2026-08-14 Release of logback version 1.6.3</h1>
<ul>
<li>
<p>In response <a
href="https://www.cve.org/cverecord?id=CVE-2026-19880">CVE-2026-19880</a>,
<code>MDCBasedDiscriminator</code> (used by
<code>SiftingAppender</code>) now strips forward and backward slashes
(<code>/</code>, <code>\</code>) from MDC values before they are used as
discriminating keys. This prevents path segments from escaping into
destinations controlled by an attacker. When sanitisation actually
changes a value, a warning is emitted; the warning is rate-limited (a
small batch, then a lull of about ten minutes).</p>
</li>
<li>
<p>Colour console support is split out into a dedicated <a
href="https://logback.qos.ch/manual/appenders.html#JansiConsoleAppender"><code>JansiConsoleAppender</code></a>.
It wraps stdout or stderr with Jansi so ANSI escape sequences (for
example coloured patterns) render correctly on terminals that need it,
notably Windows. Prefer this class over the older path described next.
See the <a
href="https://logback.qos.ch/manual/appenders.html#JansiConsoleAppender">appenders
documentation</a>.</p>
</li>
<li>
<p>The <code>withJansi</code> property on <code>ConsoleAppender</code>
is <strong>deprecated</strong>. Existing configurations that still set
<code><withJansi>true</withJansi></code> continue to work
for compatibility, but new setups should use
<code>JansiConsoleAppender</code> instead.</p>
</li>
<li>
<p><code>ConsoleAppender</code> no longer treats the process console as
an exclusive resource: stopping it does not close
<code>System.out</code> / <code>System.err</code>.
<code>JansiConsoleAppender</code> pairs each
<code>AnsiConsole.systemInstall()</code> with
<code>systemUninstall()</code> on stop, so repeated start/stop cycles do
not leave Jansi installed or tear down streams shared with the rest of
the JVM. Related behavior is covered by tests for <a
href="https://redirect.github.com/qos-ch/logback/issues/1063">issues/1063</a>.</p>
</li>
<li>
<p>Invocation throttling helpers were reworked:
<code>SimpleInvocationGate</code> is renamed
<code>FixedIntervalInvocationGate</code>, and
<code>BatchedFixedIntervalInvocationGate</code> allows a short burst of
invocations before applying a fixed lull. The sanitisation
warning above uses the batched gate.</p>
</li>
<li>
<p>The JPMS <code>module-info</code> for logback-core now exports the
<code>ch.qos.logback.core.property</code> package, which had been
missing from the module descriptor.</p>
</li>
<li>
<p>A bit-wise identical binary of this version can be reproduced by
building from <a href="https://github.com/qos-ch/logback">source
code</a> at commit <code>e8e824dede022a6d7208b36cfa875b0d1b7772f3</code>
associated with the tag <code>v_1.6.3</code>. The release was built
using Java "21" 2023-10-17 LTS build 21.0.1.+12-LTS-29 under
Linux Debian 11.6.</p>
</li>
</ul>
<p>--
Sponsoring SLF4J/logback/reload4j at <a
href="https://github.com/sponsors/qos-ch">https://github.com/sponsors/qos-ch</a></p>
<h2>Logback 1.6.2</h2>
<p><a
href="https://github.com/user-attachments/assets/9ceaf157-b758-4188-815d-edfe4e1b4edd">https://github.com/user-attachments/assets/9ceaf157-b758-4188-815d-edfe4e1b4edd</a></p>
<h1>2026-08-10 Release of logback version 1.6.2</h1>
<ul>
<li>
<p>Configuration analysis now detects <em>contradictory caller-data
inclusion instructions</em>. For example, an <code>AsyncAppender</code>,
<code>SocketAppender</code> or <code>SMTPAppender</code> with
<code>includeCallerData</code> left at the default <code>false</code> is
incompatible with a layout or encoder pattern that uses a caller-data
converter such as <code>%C</code>, <code>%M</code>, <code>%L</code>,
<code>%F</code>, <code>%l</code> or <code>%caller</code>. At runtime
those converters would print question marks and still incur extraction
cost on a worker thread. Logback now emits a configuration-time warning
when such instructions disagree. See <a
href="https://logback.qos.ch/codes.html#callerContradiction">codes.html#callerContradiction</a>
for details. This issue was reported in <a
href="https://redirect.github.com/qos-ch/logback/issues/1059">issues/1059</a>
by <a href="https://github.com/leeychee">leeychee</a>. The initial
analysis was contributed by <a
href="https://github.com/seonwooj0810">seonwoo_jung</a>.</p>
</li>
<li>
<p>Caller-contradiction analysis can be turned off by setting the
<code>logback.skipCallerContradictionAnalysis</code> variable to
<code>true</code>, either as a system property
(<code>-Dlogback.skipCallerContradictionAnalysis=true</code>) or as a
property in the configuration file:</p>
<pre lang="xml"><code><property
name="logback.skipCallerContradictionAnalysis"
value="true"/>
</code></pre>
</li>
<li>
<p><code>SimpleSocketServer</code> and
<code>SimpleSSLSocketServer</code> now require an explicit client IP
whitelist. On the command line, pass one or more allowed addresses
(single IPs or CIDR ranges) after the configuration file. An empty
whitelist means no clients are accepted. When embedding the server
programmatically, register allowed addresses with
<code>addAllowedClientAddress(String)</code> or
<code>setAllowedClientAddresses(Collection)</code> before clients
connect. See the documentation on <a
href="https://logback.qos.ch/manual/appenders.html#simpleSocketServerClientAccess">restricting
client access</a>.</p>
</li>
<li>
<p>Added <code>ThrowableProxyVOBuilder</code> for assembling a
<code>ThrowableProxyVO</code> field by field, with a corresponding
<code>ThrowableProxyVO.builder()</code> entry point.</p>
</li>
<li>
<p>Dependency analysis handlers now run their <code>postHandle</code>
method after child models have been processed, so checks that depend on
nested appenders (such as caller-contradiction analysis) see a complete
picture.</p>
</li>
<li>
<p>Updated several dependencies, including Angus Mail to 2.0.4 and Jetty
(test) to 12.1.12.</p>
</li>
<li>
<p>A bit-wise identical binary of this version can be reproduced by
building from <a href="https://github.com/qos-ch/logback">source
code</a> at commit e3d78330ad1ba024fd987fd00c3ffb9cfcdb07dc associated
with the tag <code>v_1.6.2</code>. The release was built using Java
"21" 2023-10-17 LTS build 21.0.1.+12-LTS-29 under Linux Debian
11.6.</p>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/qos-ch/logback/commit/e8e824dede022a6d7208b36cfa875b0d1b7772f3"><code>e8e824d</code></a>
prepare release 1.6.3</li>
<li><a
href="https://github.com/qos-ch/logback/commit/761821bfaacac3a0ad44fa546cfc814429bf9312"><code>761821b</code></a>
MDCBasedDiscriminator has a gated warning mechanism</li>
<li><a
href="https://github.com/qos-ch/logback/commit/53ed1229008d8b1902f5c234deaa07d742890879"><code>53ed122</code></a>
update copyright year</li>
<li><a
href="https://github.com/qos-ch/logback/commit/c7e2db244671ffa916182b5da8c89579eb54a645"><code>c7e2db2</code></a>
rename SimpleInvocationGate as FixedIntervalInvocationGate</li>
<li><a
href="https://github.com/qos-ch/logback/commit/b5aa931b096a4b0b6a9e140b74fabe7da152cbf0"><code>b5aa931</code></a>
added BatchedSimpleInvocationGate</li>
<li><a
href="https://github.com/qos-ch/logback/commit/1f22af7686aadd25c08b4bd1e6943a906a743ad4"><code>1f22af7</code></a>
add javadocs to SimpleInvocationGate</li>
<li><a
href="https://github.com/qos-ch/logback/commit/638ffa7e7852478b605a91b3e91238ff26f8158c"><code>638ffa7</code></a>
prevent forward and backward slashes to escape to other directories</li>
<li><a
href="https://github.com/qos-ch/logback/commit/7d6b9a4f8c8996834c0a694f6c141705a003d7bb"><code>7d6b9a4</code></a>
add missing ch.qos.logback.core.property package</li>
<li><a
href="https://github.com/qos-ch/logback/commit/fa25930346f35636fb6a077c1f66ebb06edd3b6f"><code>fa25930</code></a>
add an extension path in ConsoleAppender for JansiConsoleAppender</li>
<li><a
href="https://github.com/qos-ch/logback/commit/c73b43f2011f9d4545abc7ea461172276a0a43b3"><code>c73b43f</code></a>
deprecate the withJansi path</li>
<li>Additional commits viewable in <a
href="https://github.com/qos-ch/logback/compare/v_1.6.1...v_1.6.3">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>
# Description of Changes
The macOS Dock icon renders noticeably larger than every other app. The
cause is that
`icon.icns` was **100% full-bleed** - the red rounded square filled all
1024x1024 with zero
margin. macOS does not mask or inset legacy `.icns` icons, so the
artwork has to carry Apple's
grid itself: an **824x824 body centred on a 1024x1024 canvas**.
Full-bleed therefore rendered
**24% wider and 54% larger in area** than its neighbours.
Linux had the same defect for the same reason - the hicolor PNGs were
94.9-100% full-bleed,
and GNOME's HIG says an app icon is drawn within the canvas but must not
fill it (~10% margin,
so a body around 80%). Those small existing margins were resampling
artifacts, not padding.
Windows is deliberately **left full-bleed**. Microsoft imposes no inset:
target-size assets are
drawn without tile padding and the taskbar simply scales the bitmap into
the slot. `app.ico` is
a pure rename here, byte-identical to before.
## What changed
Icons are now split per platform, since the three platforms disagree
about how much of the
canvas the artwork may fill:
| Path | Owner | Treatment |
| --- | --- | --- |
| `icons/macos/app.icns` | `dmg`, `app` | 824/1024 Apple grid |
| `icons/macos/app-512.png` | build-time only | see note below |
| `icons/linux/app-{16..512}.png` | `deb`, `rpm`, `appimage` | ~10%
margin, KDE's small-size exception at 16/32 |
| `icons/windows/app.ico` | `msi`, NSIS | unchanged, full-bleed |
Linux is selected by a new `tauri.linux.conf.json`. Tauri merges
platform configs with
JSON Merge Patch (RFC 7396), so `bundle.icon` is **replaced wholesale**
rather than appended.
## Notes for reviewers
- **`icons/macos/app-512.png` is build ballast, not a real asset.**
`tauri-codegen` requires a
PNG in the icon list for every non-Windows target, with a hardcoded
fallback to
`icons/icon.png` - a file this PR deletes. Without it the build fails.
It is embedded as
`default_window_icon`, which tao's macOS backend discards
(`set_window_icon` there is a no-op:
"macOS doesn't have window icons"). Nothing renders it.
- **Linux icon order matters.** The bundler derives the hicolor
directory from each PNG's real
pixel dimensions, so `app-128.png` lands in `128x128/`. `app-512.png` is
listed first because
the first PNG in the list also becomes the window icon, which GTK does
honour.
- **`.imgbotconfig` had to be repointed.** Its previous entry named
`icons/icon.png`, a path this
PR deletes. That exclusion is load-bearing: ImgBot once optimised the
icon to an indexed
palette and `tauri::generate_context!()` rejects non-RGBA icons,
breaking the desktop build
(#6990). All 15 generated PNGs, including the eight inside the `.icns`,
are verified colour
type 6.
- **Not fixed here:** our corner radius is 14.3% of the body where macOS
and GNOME neighbours sit
near 22%, so the icon still reads squarer than its neighbours. That is a
brand-silhouette
decision rather than the sizing bug, so it was left alone.
- The 15 pre-existing unused assets (`Square*Logo.png`, `mstile-*`,
`android-chrome-*`,
`android/`, `ios/`) are untouched. No configured bundle target consumes
them.
## Verification
`task check` was **not** run - this PR touches no Java, TypeScript or
engine Python, so it
cannot exercise the change. What was verified directly instead:
- Simulated the RFC 7396 merge and Tauri's `find_icon` resolution per
platform: Windows resolves
to `app.ico`, macOS to `app.icns` plus the stub PNG, Linux to its own
six PNGs. Every path exists.
- Both configs validate against the bundled
`@tauri-apps/cli/config.schema.json`, base and merged.
- Every PNG's real dimensions match its filename, and every body
measures exactly its nominal
inset (410/512, 154/192, 102/128, 52/64, 28/32, 14/16).
- An overlay diff of the new macOS body against the old artwork shows
only 1px antialiasing
hairlines - the mark itself is unchanged, only inset.
- Pre-commit hooks pass.
---
## 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)
- [ ] 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.
## Problem
In the self-hosted build with login enabled, admin-generated invite
links point to the SPA route `/invite/<token>`, but that route is not
covered by the anonymous whitelist. Anonymous users get 401 / redirected
to `/login` before the React app can mount - even though the APIs the
page calls (`/api/v1/invite/validate`, `/api/v1/invite/accept`) are
already whitelisted. Since accepting an invite is how a *new* account is
created, requiring authentication first makes the feature unusable.
## Fix
Add `INVITE_LINK_PATTERN` (`^/invite/[^/]+/?$`) in
`RequestUriUtils.java`, matched at the end of `isPublicAuthEndpoint()` -
mirroring the existing `SHARE_LINK_PATTERN` handling. The invite data
APIs remain protected by their own token validation; only the SPA
bootstrap page becomes anonymously reachable.
## Tests
Added unit tests in `RequestUriUtilsTest.java` mirroring the share-link
tests:
- `/invite/<token>` (with/without trailing slash, with context path) ?
public
- bare `/invite` and `/invite/` ? NOT public (token segment required)
- `/invite/<token>/foo` nested paths ? NOT public
- `/inviteX` prefix over-match ? NOT public
## Verification
Pattern behavior validated against all test cases above. Live-tested on
2.14.3 self-hosted: anonymous `GET /invite/<token>` returned 401 before
the fix; the whitelisted accept flow itself (`validate` + `accept` APIs)
works anonymously end-to-end.
Auto-generated by stirlingbot[bot]
This PR updates the frontend license report based on changes to
package.json dependencies.
Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
Auto-generated by stirlingbot[bot]
This PR updates the backend license report based on dependency changes.
Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
Bumps io.swagger.core.v3:swagger-core-jakarta from 2.2.46 to 2.2.53.
[](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>
### Description of Changes
This Pull Request was automatically generated to synchronize updates to
translation files and documentation. Below are the details of the
changes made:
#### **1. Synchronization of Translation Files**
- Updated translation files
(`frontend/editor/public/locales/*/translation.toml`) to reflect changes
in the reference file `en-US/translation.toml`.
- Ensured consistency and synchronization across all supported language
files.
- Highlighted any missing or incomplete translations.
- **Format**: TOML
#### **2. Update README.md**
- Generated the translation progress table in `README.md` using
`counter_translation_v3.py`.
- Added a summary of the current translation status for all supported
languages.
- Included up-to-date statistics on translation coverage.
#### **Why these changes are necessary**
- Keeps translation files aligned with the latest reference updates.
- Ensures the documentation reflects the current translation progress.
---
Auto-generated by [create-pull-request][1].
[1]: https://github.com/peter-evans/create-pull-request
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
# Description of Changes
This PR decouples the Docker base-image preparation from the embedded
Docker image matrix builds in `.github/workflows/test-build-docker.yml`.
- Added a dedicated `prepare-base-image` job that runs once when a pull
request changes the Docker base image.
- Builds `stirling-pdf-base:pr-test` once for `linux/amd64` instead of
rebuilding the same image independently in every matrix job.
- Exports the prepared image with `docker save`, compresses it, and
uploads it as a short-lived GitHub Actions artifact.
- Added a dependency from `test-build-docker-images` to
`prepare-base-image`, while still allowing the matrix job to run when
base-image preparation is skipped.
- Each matrix entry downloads and loads the prepared Docker image when
`docker-base-changed` is enabled.
- Removed the previous per-matrix `Build base image locally` step.
- Keeps the prepared image available to the embedded Docker builds
through the local Docker daemon.
The change was made to eliminate redundant base-image builds across the
Docker test matrix. Previously, pull requests modifying `docker/base`
caused each matrix entry to build the identical base image independently
and in parallel. Preparing the image once reduces duplicated CI work,
improves consistency between matrix entries, and should reduce CI
resource usage and execution time for Docker-related pull requests.
---
## 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.
Bumps `imageioVersion` from 3.13.1 to 3.14.0.
Updates `com.twelvemonkeys.imageio:imageio-batik` from 3.13.1 to 3.14.0
Updates `com.twelvemonkeys.imageio:imageio-bmp` from 3.13.1 to 3.14.0
Updates `com.twelvemonkeys.imageio:imageio-jpeg` from 3.13.1 to 3.14.0
Updates `com.twelvemonkeys.imageio:imageio-tiff` from 3.13.1 to 3.14.0
Updates `com.twelvemonkeys.imageio:imageio-webp` from 3.13.1 to 3.14.0
Updates `com.twelvemonkeys.imageio:imageio-psd` from 3.13.1 to 3.14.0
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>
## Summary
- Add a scheduled GitHub Actions workflow that checks for the latest
stable Gradle release.
- Update the Gradle wrapper and pinned Gradle Docker build images
automatically.
- Open an automated pull request when an update is available.
- Update the developer prerequisites to Node.js 22+ and Gradle 9.0+.
## Details
The workflow runs every Monday at 03:00 UTC and can also be triggered
manually. It:
1. Resolves the latest stable Gradle version.
2. Finds the matching `gradle:<version>-jdk25` Docker image digest.
3. Updates the Gradle wrapper and Dockerfiles.
4. Verifies the resolved wrapper version and checks the resulting diff.
5. Creates or updates an automated dependency pull request.
The current wrapper and Docker image changes are included as the initial
update generated by this workflow.
## Testing
- Verified the generated changes with `git diff --check`.
- The workflow validates the wrapper version before opening the
automated pull request.
Auto-generated by stirlingbot[bot]
This PR updates the frontend license report based on changes to
package.json dependencies.
Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
Bumps the simple-java-mail group with 1 update in the / directory:
[org.simplejavamail:simple-java-mail](https://github.com/bbottema/simple-java-mail).
Bumps the simple-java-mail group with 1 update in the /app/common
directory:
[org.simplejavamail:simple-java-mail](https://github.com/bbottema/simple-java-mail).
Updates `org.simplejavamail:simple-java-mail` from 9.2.0 to 9.3.1
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/bbottema/simple-java-mail/releases">org.simplejavamail:simple-java-mail's
releases</a>.</em></p>
<blockquote>
<h2>v9.3.1</h2>
<p>Simple Java Mail 9.3.1 is a Java 8-compatible build-tool maintenance
release.</p>
<h2>Changes</h2>
<ul>
<li>Updated Maven Antrun Plugin from 3.1.0 to 3.2.0 for the JPMS
consumer-compilation check.</li>
<li>Updated Maven Dependency Plugin from 3.8.1 to 3.11.0 for
construction of the JPMS module path.</li>
</ul>
<p>These changes affect project build tooling only. This release
contains no runtime-dependency changes, public API changes, or intended
mail-sending behavior changes. Java 8 remains the minimum supported
runtime.</p>
<p>The maintenance pull requests are <a
href="https://redirect.github.com/bbottema/simple-java-mail/pull/700">#700</a>
and <a
href="https://redirect.github.com/bbottema/simple-java-mail/pull/701">#701</a>.</p>
<h2>v9.3.0</h2>
<p>Simple Java Mail 9.3.0 exposes <code>batch-module</code> as a
supported standalone Jakarta Mail orchestration API.</p>
<ul>
<li><a
href="https://redirect.github.com/bbottema/simple-java-mail/issues/698">#698</a>
adds <code>BatchTransportExecutor<K></code> for applications that
create their own <code>Session</code> and <code>MimeMessage</code>
objects without adopting <code>EmailBuilder</code> or
<code>Mailer</code>. The main <code>simple-java-mail</code> facade is
not required.</li>
<li>Register Sessions by cluster key, then run cluster-selected or
exact-Session callbacks synchronously or submit them as
<code>CompletableFuture</code> work. Each callback receives the actually
selected <code>Session</code> and connected <code>Transport</code>.</li>
<li>The facade keeps raw leases private, releases connections after
successful callbacks, invalidates them after escaping failures, resolves
OAuth2 credentials from the selected Session, and provides deterministic
graceful or forced shutdown. Its default executor is module-owned; an
injected executor remains caller-owned.</li>
<li>The existing pooled <code>Mailer</code> path and the standalone
facade now share one transport engine. <code>smtp-connection-pool</code>
remains the only physical pool owner; do not stack batch/direct
orchestration over the Jakarta <code>smtppool</code> provider.</li>
<li>The supporting chain is updated to <code>smtp-connection-pool
4.0.1</code>, <code>clustered-object-pool 4.0.3</code>, and
<code>generic-object-pool 2.4.2</code>. The published JPMS names are
<code>org.simplejavamail.batch</code>,
<code>org.simplejavamail.smtpconnectionpool</code>,
<code>org.bbottema.clusteredobjectpool</code>, and
<code>org.bbottema.genericobjectpool</code>.</li>
</ul>
<p>See the <a
href="https://www.simplejavamail.org/smtp-connection-pooling.html">SMTP
connection pooling and batch orchestration guide</a> for the comparison
matrix, ownership rules, and complete examples.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/bbottema/simple-java-mail/blob/master/RELEASE.txt">org.simplejavamail:simple-java-mail's
changelog</a>.</em></p>
<blockquote>
<p><a
href="https://www.simplejavamail.org">https://www.simplejavamail.org</a></p>
<!-- raw HTML omitted -->
<p>v9.3.0 - v9.3.2</p>
<ul>
<li><strong>v9.3.2:</strong> <a
href="https://redirect.github.com/bbottema/simple-java-mail/issues/702">#702</a>:
clarified the single-address contract of RecipientBuilder by renaming
its misleading implementation parameter and validation label; use
RecipientsBuilder for comma- or semicolon-delimited address lists.</li>
<li><strong>v9.3.1:</strong> Updated Maven Antrun Plugin from 3.1.0 to
3.2.0 (<a
href="https://redirect.github.com/bbottema/simple-java-mail/issues/700">#700</a>)
and Maven Dependency Plugin from 3.8.1 to 3.11.0 (<a
href="https://redirect.github.com/bbottema/simple-java-mail/issues/701">#701</a>),
retaining Java 8 compatibility.</li>
<li><strong>v9.3.0:</strong> <a
href="https://redirect.github.com/bbottema/simple-java-mail/issues/698">#698</a>:
exposed batch-module as a supported standalone Jakarta Mail
orchestration API with clustered and exact-Session callbacks,
asynchronous submission, automatic lease release/invalidation, and
deterministic graceful or forced shutdown.</li>
<li><strong>v9.3.0:</strong> Updated smtp-connection-pool from 3.1.0 to
4.0.1 and migrated the existing Mailer integration to its explicit
SmtpTransportLease contract. The complete generic, clustered, SMTP, and
batch dependency chain now publishes stable JPMS automatic module
names.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/bbottema/simple-java-mail/commit/e44c4c983e48ec9372ff745ccb9ab01d731867d0"><code>e44c4c9</code></a>
released 9.3.1 [skip ci]</li>
<li><a
href="https://github.com/bbottema/simple-java-mail/commit/461e5f7c3b97d4882b21bca7ecb4843530184f32"><code>461e5f7</code></a>
docs(release): prepare 9.3.1 release notes</li>
<li><a
href="https://github.com/bbottema/simple-java-mail/commit/68728f3457f7b0e85cf66e758f41985b86c4a125"><code>68728f3</code></a>
build(deps-dev): bump org.apache.maven.plugins:maven-dependency-plugin
(<a
href="https://redirect.github.com/bbottema/simple-java-mail/issues/701">#701</a>)</li>
<li><a
href="https://github.com/bbottema/simple-java-mail/commit/d491c334bea647a99538984d917cd6ffdcb3cbce"><code>d491c33</code></a>
build(deps-dev): bump org.apache.maven.plugins:maven-antrun-plugin (<a
href="https://redirect.github.com/bbottema/simple-java-mail/issues/700">#700</a>)</li>
<li><a
href="https://github.com/bbottema/simple-java-mail/commit/b3111e232a23f9187c2b0c6b98a0bb1a895d9309"><code>b3111e2</code></a>
docs(website): publish pooling guidance update [skip ci]</li>
<li><a
href="https://github.com/bbottema/simple-java-mail/commit/cb3a9a133b91372d112c1b6a07c92e721f1a6f9e"><code>cb3a9a1</code></a>
docs(website): publish pooling guide follow-up [skip ci]</li>
<li><a
href="https://github.com/bbottema/simple-java-mail/commit/29f25e83c654d5dbadd4da321ce9511a4eceeeef"><code>29f25e8</code></a>
released 9.3.0 [skip ci]</li>
<li><a
href="https://github.com/bbottema/simple-java-mail/commit/b9c0cb8b58f4477e92fdd7ea395583863f875310"><code>b9c0cb8</code></a>
feat(batch): expose standalone transport orchestration</li>
<li><a
href="https://github.com/bbottema/simple-java-mail/commit/92549b27f44a0042ca908d22c25db58ccc1e1630"><code>92549b2</code></a>
merge(release): reconcile master with develop [skip ci]</li>
<li><a
href="https://github.com/bbottema/simple-java-mail/commit/b499d76ec28e0566b2000ec6418473da66e1398a"><code>b499d76</code></a>
docs(readme): rebuild developer landing page [skip ci]</li>
<li>Additional commits viewable in <a
href="https://github.com/bbottema/simple-java-mail/compare/9.2.0...9.3.1">compare
view</a></li>
</ul>
</details>
<br />
Updates `org.simplejavamail:outlook-module` from 9.2.0 to 9.3.1
Updates `org.simplejavamail:simple-java-mail` from 9.2.0 to 9.3.1
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/bbottema/simple-java-mail/releases">org.simplejavamail:simple-java-mail's
releases</a>.</em></p>
<blockquote>
<h2>v9.3.1</h2>
<p>Simple Java Mail 9.3.1 is a Java 8-compatible build-tool maintenance
release.</p>
<h2>Changes</h2>
<ul>
<li>Updated Maven Antrun Plugin from 3.1.0 to 3.2.0 for the JPMS
consumer-compilation check.</li>
<li>Updated Maven Dependency Plugin from 3.8.1 to 3.11.0 for
construction of the JPMS module path.</li>
</ul>
<p>These changes affect project build tooling only. This release
contains no runtime-dependency changes, public API changes, or intended
mail-sending behavior changes. Java 8 remains the minimum supported
runtime.</p>
<p>The maintenance pull requests are <a
href="https://redirect.github.com/bbottema/simple-java-mail/pull/700">#700</a>
and <a
href="https://redirect.github.com/bbottema/simple-java-mail/pull/701">#701</a>.</p>
<h2>v9.3.0</h2>
<p>Simple Java Mail 9.3.0 exposes <code>batch-module</code> as a
supported standalone Jakarta Mail orchestration API.</p>
<ul>
<li><a
href="https://redirect.github.com/bbottema/simple-java-mail/issues/698">#698</a>
adds <code>BatchTransportExecutor<K></code> for applications that
create their own <code>Session</code> and <code>MimeMessage</code>
objects without adopting <code>EmailBuilder</code> or
<code>Mailer</code>. The main <code>simple-java-mail</code> facade is
not required.</li>
<li>Register Sessions by cluster key, then run cluster-selected or
exact-Session callbacks synchronously or submit them as
<code>CompletableFuture</code> work. Each callback receives the actually
selected <code>Session</code> and connected <code>Transport</code>.</li>
<li>The facade keeps raw leases private, releases connections after
successful callbacks, invalidates them after escaping failures, resolves
OAuth2 credentials from the selected Session, and provides deterministic
graceful or forced shutdown. Its default executor is module-owned; an
injected executor remains caller-owned.</li>
<li>The existing pooled <code>Mailer</code> path and the standalone
facade now share one transport engine. <code>smtp-connection-pool</code>
remains the only physical pool owner; do not stack batch/direct
orchestration over the Jakarta <code>smtppool</code> provider.</li>
<li>The supporting chain is updated to <code>smtp-connection-pool
4.0.1</code>, <code>clustered-object-pool 4.0.3</code>, and
<code>generic-object-pool 2.4.2</code>. The published JPMS names are
<code>org.simplejavamail.batch</code>,
<code>org.simplejavamail.smtpconnectionpool</code>,
<code>org.bbottema.clusteredobjectpool</code>, and
<code>org.bbottema.genericobjectpool</code>.</li>
</ul>
<p>See the <a
href="https://www.simplejavamail.org/smtp-connection-pooling.html">SMTP
connection pooling and batch orchestration guide</a> for the comparison
matrix, ownership rules, and complete examples.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/bbottema/simple-java-mail/blob/master/RELEASE.txt">org.simplejavamail:simple-java-mail's
changelog</a>.</em></p>
<blockquote>
<p><a
href="https://www.simplejavamail.org">https://www.simplejavamail.org</a></p>
<!-- raw HTML omitted -->
<p>v9.3.0 - v9.3.2</p>
<ul>
<li><strong>v9.3.2:</strong> <a
href="https://redirect.github.com/bbottema/simple-java-mail/issues/702">#702</a>:
clarified the single-address contract of RecipientBuilder by renaming
its misleading implementation parameter and validation label; use
RecipientsBuilder for comma- or semicolon-delimited address lists.</li>
<li><strong>v9.3.1:</strong> Updated Maven Antrun Plugin from 3.1.0 to
3.2.0 (<a
href="https://redirect.github.com/bbottema/simple-java-mail/issues/700">#700</a>)
and Maven Dependency Plugin from 3.8.1 to 3.11.0 (<a
href="https://redirect.github.com/bbottema/simple-java-mail/issues/701">#701</a>),
retaining Java 8 compatibility.</li>
<li><strong>v9.3.0:</strong> <a
href="https://redirect.github.com/bbottema/simple-java-mail/issues/698">#698</a>:
exposed batch-module as a supported standalone Jakarta Mail
orchestration API with clustered and exact-Session callbacks,
asynchronous submission, automatic lease release/invalidation, and
deterministic graceful or forced shutdown.</li>
<li><strong>v9.3.0:</strong> Updated smtp-connection-pool from 3.1.0 to
4.0.1 and migrated the existing Mailer integration to its explicit
SmtpTransportLease contract. The complete generic, clustered, SMTP, and
batch dependency chain now publishes stable JPMS automatic module
names.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/bbottema/simple-java-mail/commit/e44c4c983e48ec9372ff745ccb9ab01d731867d0"><code>e44c4c9</code></a>
released 9.3.1 [skip ci]</li>
<li><a
href="https://github.com/bbottema/simple-java-mail/commit/461e5f7c3b97d4882b21bca7ecb4843530184f32"><code>461e5f7</code></a>
docs(release): prepare 9.3.1 release notes</li>
<li><a
href="https://github.com/bbottema/simple-java-mail/commit/68728f3457f7b0e85cf66e758f41985b86c4a125"><code>68728f3</code></a>
build(deps-dev): bump org.apache.maven.plugins:maven-dependency-plugin
(<a
href="https://redirect.github.com/bbottema/simple-java-mail/issues/701">#701</a>)</li>
<li><a
href="https://github.com/bbottema/simple-java-mail/commit/d491c334bea647a99538984d917cd6ffdcb3cbce"><code>d491c33</code></a>
build(deps-dev): bump org.apache.maven.plugins:maven-antrun-plugin (<a
href="https://redirect.github.com/bbottema/simple-java-mail/issues/700">#700</a>)</li>
<li><a
href="https://github.com/bbottema/simple-java-mail/commit/b3111e232a23f9187c2b0c6b98a0bb1a895d9309"><code>b3111e2</code></a>
docs(website): publish pooling guidance update [skip ci]</li>
<li><a
href="https://github.com/bbottema/simple-java-mail/commit/cb3a9a133b91372d112c1b6a07c92e721f1a6f9e"><code>cb3a9a1</code></a>
docs(website): publish pooling guide follow-up [skip ci]</li>
<li><a
href="https://github.com/bbottema/simple-java-mail/commit/29f25e83c654d5dbadd4da321ce9511a4eceeeef"><code>29f25e8</code></a>
released 9.3.0 [skip ci]</li>
<li><a
href="https://github.com/bbottema/simple-java-mail/commit/b9c0cb8b58f4477e92fdd7ea395583863f875310"><code>b9c0cb8</code></a>
feat(batch): expose standalone transport orchestration</li>
<li><a
href="https://github.com/bbottema/simple-java-mail/commit/92549b27f44a0042ca908d22c25db58ccc1e1630"><code>92549b2</code></a>
merge(release): reconcile master with develop [skip ci]</li>
<li><a
href="https://github.com/bbottema/simple-java-mail/commit/b499d76ec28e0566b2000ec6418473da66e1398a"><code>b499d76</code></a>
docs(readme): rebuild developer landing page [skip ci]</li>
<li>Additional commits viewable in <a
href="https://github.com/bbottema/simple-java-mail/compare/9.2.0...9.3.1">compare
view</a></li>
</ul>
</details>
<br />
Updates `org.simplejavamail:outlook-module` from 9.2.0 to 9.3.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 <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>
Bumps the embedpdf group with 21 updates in the /frontend directory:
| Package | From | To |
| --- | --- | --- |
|
[@embedpdf/core](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/core/main)
| `2.14.4` | `2.15.0` |
|
[@embedpdf/models](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/models)
| `2.14.4` | `2.15.0` |
|
[@embedpdf/plugin-annotation](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-annotation)
| `2.14.4` | `2.15.0` |
|
[@embedpdf/plugin-attachment](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-attachment)
| `2.14.4` | `2.15.0` |
|
[@embedpdf/plugin-bookmark](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-bookmark)
| `2.14.4` | `2.15.0` |
|
[@embedpdf/plugin-document-manager](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-document-manager)
| `2.14.4` | `2.15.0` |
|
[@embedpdf/plugin-export](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-download)
| `2.14.4` | `2.15.0` |
|
[@embedpdf/plugin-history](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-history)
| `2.14.4` | `2.15.0` |
|
[@embedpdf/plugin-interaction-manager](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-interaction-manager)
| `2.14.4` | `2.15.0` |
|
[@embedpdf/plugin-pan](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-pan)
| `2.14.4` | `2.15.0` |
|
[@embedpdf/plugin-print](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-print)
| `2.14.4` | `2.15.0` |
|
[@embedpdf/plugin-redaction](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-redaction)
| `2.14.4` | `2.15.0` |
|
[@embedpdf/plugin-render](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-render)
| `2.14.4` | `2.15.0` |
|
[@embedpdf/plugin-rotate](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-rotate)
| `2.14.4` | `2.15.0` |
|
[@embedpdf/plugin-scroll](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-scroll)
| `2.14.4` | `2.15.0` |
|
[@embedpdf/plugin-search](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-search)
| `2.14.4` | `2.15.0` |
|
[@embedpdf/plugin-spread](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-spread)
| `2.14.4` | `2.15.0` |
|
[@embedpdf/plugin-thumbnail](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-thumbnail)
| `2.14.4` | `2.15.0` |
|
[@embedpdf/plugin-tiling](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-tiling)
| `2.14.4` | `2.15.0` |
|
[@embedpdf/plugin-viewport](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-viewport)
| `2.14.4` | `2.15.0` |
|
[@embedpdf/plugin-zoom](https://github.com/embedpdf/embed-pdf-viewer/tree/HEAD/packages/plugin-zoom)
| `2.14.4` | `2.15.0` |
Updates `@embedpdf/core` from 2.14.4 to 2.15.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/embedpdf/embed-pdf-viewer/releases">@embedpdf/core's
releases</a>.</em></p>
<blockquote>
<h2>Release v2.15.0</h2>
<h2><code>@embedpdf/plugin-selection</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h3>Minor Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/embedpdf/embed-pdf-viewer/pull/685">#685</a>
by <a href="https://github.com/simonmysun"><code>@simonmysun</code></a>
– Add <code>setSelection(range, documentId?)</code> to the selection
capability and document scope for programmatically applying or restoring
a text selection.</p>
<p>It accepts the same <code>SelectionRangeX</code> (<code>{ start, end
}</code> glyph pointers) shape emitted by
<code>onSelectionChange</code>, so a saved selection can be passed
straight back in to restore it; passing <code>null</code> clears the
selection. Page geometry is loaded on demand, so the returned task
resolves only once the highlight rects are computed. The range is
normalized (start/end may be given in any order), invalid input
(malformed range, non-integer/negative indices, out-of-bounds pages) is
rejected, glyph indices are clamped to the available page geometry, and
previously highlighted pages are repainted so switching to a disjoint
selection no longer leaves stale highlights behind.</p>
</li>
</ul>
<h2><code>@embedpdf/core</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/embedpdf/embed-pdf-viewer/pull/703">#703</a>
by <a
href="https://github.com/berkayozdin"><code>@berkayozdin</code></a> –
Make the precompiled Svelte output work on every Svelte 5 runtime</p>
<p>Svelte 5.56 changed the <code>exclude</code> argument of the private
<code>rest_props</code> runtime helper from an array
(<code>exclude.includes(key)</code>) to a <code>Set</code>
(<code>exclude.has(key)</code>). The <code>*/svelte</code> entry points
ship precompiled component code, so output built against one side of
that change throws on the other: the currently published packages fail
with <code>TypeError: exclude.has is not a function</code> on Svelte
>= 5.56, which aborts the render of every EmbedPDF Svelte
component.</p>
<p>The Svelte build now routes those calls through a wrapper that hands
the runtime an <code>exclude</code> value satisfying both contracts, so
one published build stays valid across the whole <code>svelte:
">=5 <6"</code> peer range.</p>
</li>
</ul>
<h2><code>@embedpdf/engines</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/models</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/pdfium</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-annotation</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-attachment</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-bookmark</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-capture</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-commands</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-document-manager</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-export</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-form</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/embedpdf/embed-pdf-viewer/commits/v2.15.0/packages/core/main">compare
view</a></li>
</ul>
</details>
<br />
Updates `@embedpdf/engines` from 2.14.4 to 2.15.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/embedpdf/embed-pdf-viewer/releases">@embedpdf/engines's
releases</a>.</em></p>
<blockquote>
<h2>Release v2.15.0</h2>
<h2><code>@embedpdf/plugin-selection</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h3>Minor Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/embedpdf/embed-pdf-viewer/pull/685">#685</a>
by <a href="https://github.com/simonmysun"><code>@simonmysun</code></a>
– Add <code>setSelection(range, documentId?)</code> to the selection
capability and document scope for programmatically applying or restoring
a text selection.</p>
<p>It accepts the same <code>SelectionRangeX</code> (<code>{ start, end
}</code> glyph pointers) shape emitted by
<code>onSelectionChange</code>, so a saved selection can be passed
straight back in to restore it; passing <code>null</code> clears the
selection. Page geometry is loaded on demand, so the returned task
resolves only once the highlight rects are computed. The range is
normalized (start/end may be given in any order), invalid input
(malformed range, non-integer/negative indices, out-of-bounds pages) is
rejected, glyph indices are clamped to the available page geometry, and
previously highlighted pages are repainted so switching to a disjoint
selection no longer leaves stale highlights behind.</p>
</li>
</ul>
<h2><code>@embedpdf/core</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/embedpdf/embed-pdf-viewer/pull/703">#703</a>
by <a
href="https://github.com/berkayozdin"><code>@berkayozdin</code></a> –
Make the precompiled Svelte output work on every Svelte 5 runtime</p>
<p>Svelte 5.56 changed the <code>exclude</code> argument of the private
<code>rest_props</code> runtime helper from an array
(<code>exclude.includes(key)</code>) to a <code>Set</code>
(<code>exclude.has(key)</code>). The <code>*/svelte</code> entry points
ship precompiled component code, so output built against one side of
that change throws on the other: the currently published packages fail
with <code>TypeError: exclude.has is not a function</code> on Svelte
>= 5.56, which aborts the render of every EmbedPDF Svelte
component.</p>
<p>The Svelte build now routes those calls through a wrapper that hands
the runtime an <code>exclude</code> value satisfying both contracts, so
one published build stays valid across the whole <code>svelte:
">=5 <6"</code> peer range.</p>
</li>
</ul>
<h2><code>@embedpdf/engines</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/models</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/pdfium</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-annotation</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-attachment</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-bookmark</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-capture</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-commands</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-document-manager</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-export</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-form</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/embedpdf/embed-pdf-viewer/blob/v2.15.0/packages/engines/CHANGELOG.md">@embedpdf/engines's
changelog</a>.</em></p>
<blockquote>
<h2>2.15.0</h2>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/embedpdf/embed-pdf-viewer/commit/a8faf04b15291c895118b819d7395c3bca8a4959"><code>a8faf04</code></a>
chore: version packages</li>
<li>See full diff in <a
href="https://github.com/embedpdf/embed-pdf-viewer/commits/v2.15.0/packages/engines">compare
view</a></li>
</ul>
</details>
<br />
Updates `@embedpdf/models` from 2.14.4 to 2.15.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/embedpdf/embed-pdf-viewer/releases">@embedpdf/models's
releases</a>.</em></p>
<blockquote>
<h2>Release v2.15.0</h2>
<h2><code>@embedpdf/plugin-selection</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h3>Minor Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/embedpdf/embed-pdf-viewer/pull/685">#685</a>
by <a href="https://github.com/simonmysun"><code>@simonmysun</code></a>
– Add <code>setSelection(range, documentId?)</code> to the selection
capability and document scope for programmatically applying or restoring
a text selection.</p>
<p>It accepts the same <code>SelectionRangeX</code> (<code>{ start, end
}</code> glyph pointers) shape emitted by
<code>onSelectionChange</code>, so a saved selection can be passed
straight back in to restore it; passing <code>null</code> clears the
selection. Page geometry is loaded on demand, so the returned task
resolves only once the highlight rects are computed. The range is
normalized (start/end may be given in any order), invalid input
(malformed range, non-integer/negative indices, out-of-bounds pages) is
rejected, glyph indices are clamped to the available page geometry, and
previously highlighted pages are repainted so switching to a disjoint
selection no longer leaves stale highlights behind.</p>
</li>
</ul>
<h2><code>@embedpdf/core</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/embedpdf/embed-pdf-viewer/pull/703">#703</a>
by <a
href="https://github.com/berkayozdin"><code>@berkayozdin</code></a> –
Make the precompiled Svelte output work on every Svelte 5 runtime</p>
<p>Svelte 5.56 changed the <code>exclude</code> argument of the private
<code>rest_props</code> runtime helper from an array
(<code>exclude.includes(key)</code>) to a <code>Set</code>
(<code>exclude.has(key)</code>). The <code>*/svelte</code> entry points
ship precompiled component code, so output built against one side of
that change throws on the other: the currently published packages fail
with <code>TypeError: exclude.has is not a function</code> on Svelte
>= 5.56, which aborts the render of every EmbedPDF Svelte
component.</p>
<p>The Svelte build now routes those calls through a wrapper that hands
the runtime an <code>exclude</code> value satisfying both contracts, so
one published build stays valid across the whole <code>svelte:
">=5 <6"</code> peer range.</p>
</li>
</ul>
<h2><code>@embedpdf/engines</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/models</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/pdfium</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-annotation</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-attachment</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-bookmark</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-capture</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-commands</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-document-manager</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-export</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-form</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/embedpdf/embed-pdf-viewer/blob/v2.15.0/packages/models/CHANGELOG.md">@embedpdf/models's
changelog</a>.</em></p>
<blockquote>
<h2>2.15.0</h2>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/embedpdf/embed-pdf-viewer/commit/a8faf04b15291c895118b819d7395c3bca8a4959"><code>a8faf04</code></a>
chore: version packages</li>
<li>See full diff in <a
href="https://github.com/embedpdf/embed-pdf-viewer/commits/v2.15.0/packages/models">compare
view</a></li>
</ul>
</details>
<br />
Updates `@embedpdf/plugin-annotation` from 2.14.4 to 2.15.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/embedpdf/embed-pdf-viewer/releases">@embedpdf/plugin-annotation's
releases</a>.</em></p>
<blockquote>
<h2>Release v2.15.0</h2>
<h2><code>@embedpdf/plugin-selection</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h3>Minor Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/embedpdf/embed-pdf-viewer/pull/685">#685</a>
by <a href="https://github.com/simonmysun"><code>@simonmysun</code></a>
– Add <code>setSelection(range, documentId?)</code> to the selection
capability and document scope for programmatically applying or restoring
a text selection.</p>
<p>It accepts the same <code>SelectionRangeX</code> (<code>{ start, end
}</code> glyph pointers) shape emitted by
<code>onSelectionChange</code>, so a saved selection can be passed
straight back in to restore it; passing <code>null</code> clears the
selection. Page geometry is loaded on demand, so the returned task
resolves only once the highlight rects are computed. The range is
normalized (start/end may be given in any order), invalid input
(malformed range, non-integer/negative indices, out-of-bounds pages) is
rejected, glyph indices are clamped to the available page geometry, and
previously highlighted pages are repainted so switching to a disjoint
selection no longer leaves stale highlights behind.</p>
</li>
</ul>
<h2><code>@embedpdf/core</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/embedpdf/embed-pdf-viewer/pull/703">#703</a>
by <a
href="https://github.com/berkayozdin"><code>@berkayozdin</code></a> –
Make the precompiled Svelte output work on every Svelte 5 runtime</p>
<p>Svelte 5.56 changed the <code>exclude</code> argument of the private
<code>rest_props</code> runtime helper from an array
(<code>exclude.includes(key)</code>) to a <code>Set</code>
(<code>exclude.has(key)</code>). The <code>*/svelte</code> entry points
ship precompiled component code, so output built against one side of
that change throws on the other: the currently published packages fail
with <code>TypeError: exclude.has is not a function</code> on Svelte
>= 5.56, which aborts the render of every EmbedPDF Svelte
component.</p>
<p>The Svelte build now routes those calls through a wrapper that hands
the runtime an <code>exclude</code> value satisfying both contracts, so
one published build stays valid across the whole <code>svelte:
">=5 <6"</code> peer range.</p>
</li>
</ul>
<h2><code>@embedpdf/engines</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/models</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/pdfium</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-annotation</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-attachment</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-bookmark</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-capture</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-commands</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-document-manager</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-export</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-form</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/embedpdf/embed-pdf-viewer/blob/v2.15.0/packages/plugin-annotation/CHANGELOG.md">@embedpdf/plugin-annotation's
changelog</a>.</em></p>
<blockquote>
<h2>2.15.0</h2>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/embedpdf/embed-pdf-viewer/commit/a8faf04b15291c895118b819d7395c3bca8a4959"><code>a8faf04</code></a>
chore: version packages</li>
<li>See full diff in <a
href="https://github.com/embedpdf/embed-pdf-viewer/commits/v2.15.0/packages/plugin-annotation">compare
view</a></li>
</ul>
</details>
<br />
Updates `@embedpdf/plugin-attachment` from 2.14.4 to 2.15.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/embedpdf/embed-pdf-viewer/releases">@embedpdf/plugin-attachment's
releases</a>.</em></p>
<blockquote>
<h2>Release v2.15.0</h2>
<h2><code>@embedpdf/plugin-selection</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h3>Minor Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/embedpdf/embed-pdf-viewer/pull/685">#685</a>
by <a href="https://github.com/simonmysun"><code>@simonmysun</code></a>
– Add <code>setSelection(range, documentId?)</code> to the selection
capability and document scope for programmatically applying or restoring
a text selection.</p>
<p>It accepts the same <code>SelectionRangeX</code> (<code>{ start, end
}</code> glyph pointers) shape emitted by
<code>onSelectionChange</code>, so a saved selection can be passed
straight back in to restore it; passing <code>null</code> clears the
selection. Page geometry is loaded on demand, so the returned task
resolves only once the highlight rects are computed. The range is
normalized (start/end may be given in any order), invalid input
(malformed range, non-integer/negative indices, out-of-bounds pages) is
rejected, glyph indices are clamped to the available page geometry, and
previously highlighted pages are repainted so switching to a disjoint
selection no longer leaves stale highlights behind.</p>
</li>
</ul>
<h2><code>@embedpdf/core</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/embedpdf/embed-pdf-viewer/pull/703">#703</a>
by <a
href="https://github.com/berkayozdin"><code>@berkayozdin</code></a> –
Make the precompiled Svelte output work on every Svelte 5 runtime</p>
<p>Svelte 5.56 changed the <code>exclude</code> argument of the private
<code>rest_props</code> runtime helper from an array
(<code>exclude.includes(key)</code>) to a <code>Set</code>
(<code>exclude.has(key)</code>). The <code>*/svelte</code> entry points
ship precompiled component code, so output built against one side of
that change throws on the other: the currently published packages fail
with <code>TypeError: exclude.has is not a function</code> on Svelte
>= 5.56, which aborts the render of every EmbedPDF Svelte
component.</p>
<p>The Svelte build now routes those calls through a wrapper that hands
the runtime an <code>exclude</code> value satisfying both contracts, so
one published build stays valid across the whole <code>svelte:
">=5 <6"</code> peer range.</p>
</li>
</ul>
<h2><code>@embedpdf/engines</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/models</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/pdfium</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-annotation</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-attachment</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-bookmark</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-capture</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-commands</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-document-manager</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-export</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-form</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/embedpdf/embed-pdf-viewer/blob/v2.15.0/packages/plugin-attachment/CHANGELOG.md">@embedpdf/plugin-attachment's
changelog</a>.</em></p>
<blockquote>
<h2>2.15.0</h2>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/embedpdf/embed-pdf-viewer/commit/a8faf04b15291c895118b819d7395c3bca8a4959"><code>a8faf04</code></a>
chore: version packages</li>
<li>See full diff in <a
href="https://github.com/embedpdf/embed-pdf-viewer/commits/v2.15.0/packages/plugin-attachment">compare
view</a></li>
</ul>
</details>
<br />
Updates `@embedpdf/plugin-bookmark` from 2.14.4 to 2.15.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/embedpdf/embed-pdf-viewer/releases">@embedpdf/plugin-bookmark's
releases</a>.</em></p>
<blockquote>
<h2>Release v2.15.0</h2>
<h2><code>@embedpdf/plugin-selection</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h3>Minor Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/embedpdf/embed-pdf-viewer/pull/685">#685</a>
by <a href="https://github.com/simonmysun"><code>@simonmysun</code></a>
– Add <code>setSelection(range, documentId?)</code> to the selection
capability and document scope for programmatically applying or restoring
a text selection.</p>
<p>It accepts the same <code>SelectionRangeX</code> (<code>{ start, end
}</code> glyph pointers) shape emitted by
<code>onSelectionChange</code>, so a saved selection can be passed
straight back in to restore it; passing <code>null</code> clears the
selection. Page geometry is loaded on demand, so the returned task
resolves only once the highlight rects are computed. The range is
normalized (start/end may be given in any order), invalid input
(malformed range, non-integer/negative indices, out-of-bounds pages) is
rejected, glyph indices are clamped to the available page geometry, and
previously highlighted pages are repainted so switching to a disjoint
selection no longer leaves stale highlights behind.</p>
</li>
</ul>
<h2><code>@embedpdf/core</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/embedpdf/embed-pdf-viewer/pull/703">#703</a>
by <a
href="https://github.com/berkayozdin"><code>@berkayozdin</code></a> –
Make the precompiled Svelte output work on every Svelte 5 runtime</p>
<p>Svelte 5.56 changed the <code>exclude</code> argument of the private
<code>rest_props</code> runtime helper from an array
(<code>exclude.includes(key)</code>) to a <code>Set</code>
(<code>exclude.has(key)</code>). The <code>*/svelte</code> entry points
ship precompiled component code, so output built against one side of
that change throws on the other: the currently published packages fail
with <code>TypeError: exclude.has is not a function</code> on Svelte
>= 5.56, which aborts the render of every EmbedPDF Svelte
component.</p>
<p>The Svelte build now routes those calls through a wrapper that hands
the runtime an <code>exclude</code> value satisfying both contracts, so
one published build stays valid across the whole <code>svelte:
">=5 <6"</code> peer range.</p>
</li>
</ul>
<h2><code>@embedpdf/engines</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/models</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/pdfium</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-annotation</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-attachment</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-bookmark</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-capture</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-commands</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-document-manager</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-export</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-form</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/embedpdf/embed-pdf-viewer/blob/v2.15.0/packages/plugin-bookmark/CHANGELOG.md">@embedpdf/plugin-bookmark's
changelog</a>.</em></p>
<blockquote>
<h2>2.15.0</h2>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/embedpdf/embed-pdf-viewer/commit/a8faf04b15291c895118b819d7395c3bca8a4959"><code>a8faf04</code></a>
chore: version packages</li>
<li>See full diff in <a
href="https://github.com/embedpdf/embed-pdf-viewer/commits/v2.15.0/packages/plugin-bookmark">compare
view</a></li>
</ul>
</details>
<br />
Updates `@embedpdf/plugin-document-manager` from 2.14.4 to 2.15.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/embedpdf/embed-pdf-viewer/releases">@embedpdf/plugin-document-manager's
releases</a>.</em></p>
<blockquote>
<h2>Release v2.15.0</h2>
<h2><code>@embedpdf/plugin-selection</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h3>Minor Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/embedpdf/embed-pdf-viewer/pull/685">#685</a>
by <a href="https://github.com/simonmysun"><code>@simonmysun</code></a>
– Add <code>setSelection(range, documentId?)</code> to the selection
capability and document scope for programmatically applying or restoring
a text selection.</p>
<p>It accepts the same <code>SelectionRangeX</code> (<code>{ start, end
}</code> glyph pointers) shape emitted by
<code>onSelectionChange</code>, so a saved selection can be passed
straight back in to restore it; passing <code>null</code> clears the
selection. Page geometry is loaded on demand, so the returned task
resolves only once the highlight rects are computed. The range is
normalized (start/end may be given in any order), invalid input
(malformed range, non-integer/negative indices, out-of-bounds pages) is
rejected, glyph indices are clamped to the available page geometry, and
previously highlighted pages are repainted so switching to a disjoint
selection no longer leaves stale highlights behind.</p>
</li>
</ul>
<h2><code>@embedpdf/core</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/embedpdf/embed-pdf-viewer/pull/703">#703</a>
by <a
href="https://github.com/berkayozdin"><code>@berkayozdin</code></a> –
Make the precompiled Svelte output work on every Svelte 5 runtime</p>
<p>Svelte 5.56 changed the <code>exclude</code> argument of the private
<code>rest_props</code> runtime helper from an array
(<code>exclude.includes(key)</code>) to a <code>Set</code>
(<code>exclude.has(key)</code>). The <code>*/svelte</code> entry points
ship precompiled component code, so output built against one side of
that change throws on the other: the currently published packages fail
with <code>TypeError: exclude.has is not a function</code> on Svelte
>= 5.56, which aborts the render of every EmbedPDF Svelte
component.</p>
<p>The Svelte build now routes those calls through a wrapper that hands
the runtime an <code>exclude</code> value satisfying both contracts, so
one published build stays valid across the whole <code>svelte:
">=5 <6"</code> peer range.</p>
</li>
</ul>
<h2><code>@embedpdf/engines</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/models</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/pdfium</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-annotation</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-attachment</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-bookmark</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-capture</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-commands</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-document-manager</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-export</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-form</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/embedpdf/embed-pdf-viewer/blob/v2.15.0/packages/plugin-document-manager/CHANGELOG.md">@embedpdf/plugin-document-manager's
changelog</a>.</em></p>
<blockquote>
<h2>2.15.0</h2>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/embedpdf/embed-pdf-viewer/commit/a8faf04b15291c895118b819d7395c3bca8a4959"><code>a8faf04</code></a>
chore: version packages</li>
<li>See full diff in <a
href="https://github.com/embedpdf/embed-pdf-viewer/commits/v2.15.0/packages/plugin-document-manager">compare
view</a></li>
</ul>
</details>
<br />
Updates `@embedpdf/plugin-export` from 2.14.4 to 2.15.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/embedpdf/embed-pdf-viewer/releases">@embedpdf/plugin-export's
releases</a>.</em></p>
<blockquote>
<h2>Release v2.15.0</h2>
<h2><code>@embedpdf/plugin-selection</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h3>Minor Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/embedpdf/embed-pdf-viewer/pull/685">#685</a>
by <a href="https://github.com/simonmysun"><code>@simonmysun</code></a>
– Add <code>setSelection(range, documentId?)</code> to the selection
capability and document scope for programmatically applying or restoring
a text selection.</p>
<p>It accepts the same <code>SelectionRangeX</code> (<code>{ start, end
}</code> glyph pointers) shape emitted by
<code>onSelectionChange</code>, so a saved selection can be passed
straight back in to restore it; passing <code>null</code> clears the
selection. Page geometry is loaded on demand, so the returned task
resolves only once the highlight rects are computed. The range is
normalized (start/end may be given in any order), invalid input
(malformed range, non-integer/negative indices, out-of-bounds pages) is
rejected, glyph indices are clamped to the available page geometry, and
previously highlighted pages are repainted so switching to a disjoint
selection no longer leaves stale highlights behind.</p>
</li>
</ul>
<h2><code>@embedpdf/core</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/embedpdf/embed-pdf-viewer/pull/703">#703</a>
by <a
href="https://github.com/berkayozdin"><code>@berkayozdin</code></a> –
Make the precompiled Svelte output work on every Svelte 5 runtime</p>
<p>Svelte 5.56 changed the <code>exclude</code> argument of the private
<code>rest_props</code> runtime helper from an array
(<code>exclude.includes(key)</code>) to a <code>Set</code>
(<code>exclude.has(key)</code>). The <code>*/svelte</code> entry points
ship precompiled component code, so output built against one side of
that change throws on the other: the currently published packages fail
with <code>TypeError: exclude.has is not a function</code> on Svelte
>= 5.56, which aborts the render of every EmbedPDF Svelte
component.</p>
<p>The Svelte build now routes those calls through a wrapper that hands
the runtime an <code>exclude</code> value satisfying both contracts, so
one published build stays valid across the whole <code>svelte:
">=5 <6"</code> peer range.</p>
</li>
</ul>
<h2><code>@embedpdf/engines</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/models</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/pdfium</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-annotation</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-attachment</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-bookmark</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-capture</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-commands</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-document-manager</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-export</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-form</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/embedpdf/embed-pdf-viewer/commits/v2.15.0/packages/plugin-download">compare
view</a></li>
</ul>
</details>
<br />
Updates `@embedpdf/plugin-history` from 2.14.4 to 2.15.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/embedpdf/embed-pdf-viewer/releases">@embedpdf/plugin-history's
releases</a>.</em></p>
<blockquote>
<h2>Release v2.15.0</h2>
<h2><code>@embedpdf/plugin-selection</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h3>Minor Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/embedpdf/embed-pdf-viewer/pull/685">#685</a>
by <a href="https://github.com/simonmysun"><code>@simonmysun</code></a>
– Add <code>setSelection(range, documentId?)</code> to the selection
capability and document scope for programmatically applying or restoring
a text selection.</p>
<p>It accepts the same <code>SelectionRangeX</code> (<code>{ start, end
}</code> glyph pointers) shape emitted by
<code>onSelectionChange</code>, so a saved selection can be passed
straight back in to restore it; passing <code>null</code> clears the
selection. Page geometry is loaded on demand, so the returned task
resolves only once the highlight rects are computed. The range is
normalized (start/end may be given in any order), invalid input
(malformed range, non-integer/negative indices, out-of-bounds pages) is
rejected, glyph indices are clamped to the available page geometry, and
previously highlighted pages are repainted so switching to a disjoint
selection no longer leaves stale highlights behind.</p>
</li>
</ul>
<h2><code>@embedpdf/core</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/embedpdf/embed-pdf-viewer/pull/703">#703</a>
by <a
href="https://github.com/berkayozdin"><code>@berkayozdin</code></a> –
Make the precompiled Svelte output work on every Svelte 5 runtime</p>
<p>Svelte 5.56 changed the <code>exclude</code> argument of the private
<code>rest_props</code> runtime helper from an array
(<code>exclude.includes(key)</code>) to a <code>Set</code>
(<code>exclude.has(key)</code>). The <code>*/svelte</code> entry points
ship precompiled component code, so output built against one side of
that change throws on the other: the currently published packages fail
with <code>TypeError: exclude.has is not a function</code> on Svelte
>= 5.56, which aborts the render of every EmbedPDF Svelte
component.</p>
<p>The Svelte build now routes those calls through a wrapper that hands
the runtime an <code>exclude</code> value satisfying both contracts, so
one published build stays valid across the whole <code>svelte:
">=5 <6"</code> peer range.</p>
</li>
</ul>
<h2><code>@embedpdf/engines</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/models</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/pdfium</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-annotation</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-attachment</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-bookmark</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-capture</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-commands</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-document-manager</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-export</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-form</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/embedpdf/embed-pdf-viewer/blob/v2.15.0/packages/plugin-history/CHANGELOG.md">@embedpdf/plugin-history's
changelog</a>.</em></p>
<blockquote>
<h2>2.15.0</h2>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/embedpdf/embed-pdf-viewer/commit/a8faf04b15291c895118b819d7395c3bca8a4959"><code>a8faf04</code></a>
chore: version packages</li>
<li>See full diff in <a
href="https://github.com/embedpdf/embed-pdf-viewer/commits/v2.15.0/packages/plugin-history">compare
view</a></li>
</ul>
</details>
<br />
Updates `@embedpdf/plugin-interaction-manager` from 2.14.4 to 2.15.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/embedpdf/embed-pdf-viewer/releases">@embedpdf/plugin-interaction-manager's
releases</a>.</em></p>
<blockquote>
<h2>Release v2.15.0</h2>
<h2><code>@embedpdf/plugin-selection</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h3>Minor Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/embedpdf/embed-pdf-viewer/pull/685">#685</a>
by <a href="https://github.com/simonmysun"><code>@simonmysun</code></a>
– Add <code>setSelection(range, documentId?)</code> to the selection
capability and document scope for programmatically applying or restoring
a text selection.</p>
<p>It accepts the same <code>SelectionRangeX</code> (<code>{ start, end
}</code> glyph pointers) shape emitted by
<code>onSelectionChange</code>, so a saved selection can be passed
straight back in to restore it; passing <code>null</code> clears the
selection. Page geometry is loaded on demand, so the returned task
resolves only once the highlight rects are computed. The range is
normalized (start/end may be given in any order), invalid input
(malformed range, non-integer/negative indices, out-of-bounds pages) is
rejected, glyph indices are clamped to the available page geometry, and
previously highlighted pages are repainted so switching to a disjoint
selection no longer leaves stale highlights behind.</p>
</li>
</ul>
<h2><code>@embedpdf/core</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/embedpdf/embed-pdf-viewer/pull/703">#703</a>
by <a
href="https://github.com/berkayozdin"><code>@berkayozdin</code></a> –
Make the precompiled Svelte output work on every Svelte 5 runtime</p>
<p>Svelte 5.56 changed the <code>exclude</code> argument of the private
<code>rest_props</code> runtime helper from an array
(<code>exclude.includes(key)</code>) to a <code>Set</code>
(<code>exclude.has(key)</code>). The <code>*/svelte</code> entry points
ship precompiled component code, so output built against one side of
that change throws on the other: the currently published packages fail
with <code>TypeError: exclude.has is not a function</code> on Svelte
>= 5.56, which aborts the render of every EmbedPDF Svelte
component.</p>
<p>The Svelte build now routes those calls through a wrapper that hands
the runtime an <code>exclude</code> value satisfying both contracts, so
one published build stays valid across the whole <code>svelte:
">=5 <6"</code> peer range.</p>
</li>
</ul>
<h2><code>@embedpdf/engines</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/models</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/pdfium</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-annotation</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-attachment</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-bookmark</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-capture</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-commands</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-document-manager</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-export</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-form</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/embedpdf/embed-pdf-viewer/blob/v2.15.0/packages/plugin-interaction-manager/CHANGELOG.md">@embedpdf/plugin-interaction-manager's
changelog</a>.</em></p>
<blockquote>
<h2>2.15.0</h2>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/embedpdf/embed-pdf-viewer/commit/a8faf04b15291c895118b819d7395c3bca8a4959"><code>a8faf04</code></a>
chore: version packages</li>
<li>See full diff in <a
href="https://github.com/embedpdf/embed-pdf-viewer/commits/v2.15.0/packages/plugin-interaction-manager">compare
view</a></li>
</ul>
</details>
<br />
Updates `@embedpdf/plugin-pan` from 2.14.4 to 2.15.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/embedpdf/embed-pdf-viewer/releases">@embedpdf/plugin-pan's
releases</a>.</em></p>
<blockquote>
<h2>Release v2.15.0</h2>
<h2><code>@embedpdf/plugin-selection</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h3>Minor Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/embedpdf/embed-pdf-viewer/pull/685">#685</a>
by <a href="https://github.com/simonmysun"><code>@simonmysun</code></a>
– Add <code>setSelection(range, documentId?)</code> to the selection
capability and document scope for programmatically applying or restoring
a text selection.</p>
<p>It accepts the same <code>SelectionRangeX</code> (<code>{ start, end
}</code> glyph pointers) shape emitted by
<code>onSelectionChange</code>, so a saved selection can be passed
straight back in to restore it; passing <code>null</code> clears the
selection. Page geometry is loaded on demand, so the returned task
resolves only once the highlight rects are computed. The range is
normalized (start/end may be given in any order), invalid input
(malformed range, non-integer/negative indices, out-of-bounds pages) is
rejected, glyph indices are clamped to the available page geometry, and
previously highlighted pages are repainted so switching to a disjoint
selection no longer leaves stale highlights behind.</p>
</li>
</ul>
<h2><code>@embedpdf/core</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/embedpdf/embed-pdf-viewer/pull/703">#703</a>
by <a
href="https://github.com/berkayozdin"><code>@berkayozdin</code></a> –
Make the precompiled Svelte output work on every Svelte 5 runtime</p>
<p>Svelte 5.56 changed the <code>exclude</code> argument of the private
<code>rest_props</code> runtime helper from an array
(<code>exclude.includes(key)</code>) to a <code>Set</code>
(<code>exclude.has(key)</code>). The <code>*/svelte</code> entry points
ship precompiled component code, so output built against one side of
that change throws on the other: the currently published packages fail
with <code>TypeError: exclude.has is not a function</code> on Svelte
>= 5.56, which aborts the render of every EmbedPDF Svelte
component.</p>
<p>The Svelte build now routes those calls through a wrapper that hands
the runtime an <code>exclude</code> value satisfying both contracts, so
one published build stays valid across the whole <code>svelte:
">=5 <6"</code> peer range.</p>
</li>
</ul>
<h2><code>@embedpdf/engines</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/models</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/pdfium</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-annotation</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-attachment</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-bookmark</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-capture</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-commands</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-document-manager</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-export</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-form</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/embedpdf/embed-pdf-viewer/blob/v2.15.0/packages/plugin-pan/CHANGELOG.md">@embedpdf/plugin-pan's
changelog</a>.</em></p>
<blockquote>
<h2>2.15.0</h2>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/embedpdf/embed-pdf-viewer/commit/a8faf04b15291c895118b819d7395c3bca8a4959"><code>a8faf04</code></a>
chore: version packages</li>
<li>See full diff in <a
href="https://github.com/embedpdf/embed-pdf-viewer/commits/v2.15.0/packages/plugin-pan">compare
view</a></li>
</ul>
</details>
<br />
Updates `@embedpdf/plugin-print` from 2.14.4 to 2.15.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/embedpdf/embed-pdf-viewer/releases">@embedpdf/plugin-print's
releases</a>.</em></p>
<blockquote>
<h2>Release v2.15.0</h2>
<h2><code>@embedpdf/plugin-selection</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h3>Minor Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/embedpdf/embed-pdf-viewer/pull/685">#685</a>
by <a href="https://github.com/simonmysun"><code>@simonmysun</code></a>
– Add <code>setSelection(range, documentId?)</code> to the selection
capability and document scope for programmatically applying or restoring
a text selection.</p>
<p>It accepts the same <code>SelectionRangeX</code> (<code>{ start, end
}</code> glyph pointers) shape emitted by
<code>onSelectionChange</code>, so a saved selection can be passed
straight back in to restore it; passing <code>null</code> clears the
selection. Page geometry is loaded on demand, so the returned task
resolves only once the highlight rects are computed. The range is
normalized (start/end may be given in any order), invalid input
(malformed range, non-integer/negative indices, out-of-bounds pages) is
rejected, glyph indices are clamped to the available page geometry, and
previously highlighted pages are repainted so switching to a disjoint
selection no longer leaves stale highlights behind.</p>
</li>
</ul>
<h2><code>@embedpdf/core</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/embedpdf/embed-pdf-viewer/pull/703">#703</a>
by <a
href="https://github.com/berkayozdin"><code>@berkayozdin</code></a> –
Make the precompiled Svelte output work on every Svelte 5 runtime</p>
<p>Svelte 5.56 changed the <code>exclude</code> argument of the private
<code>rest_props</code> runtime helper from an array
(<code>exclude.includes(key)</code>) to a <code>Set</code>
(<code>exclude.has(key)</code>). The <code>*/svelte</code> entry points
ship precompiled component code, so output built against one side of
that change throws on the other: the currently published packages fail
with <code>TypeError: exclude.has is not a function</code> on Svelte
>= 5.56, which aborts the render of every EmbedPDF Svelte
component.</p>
<p>The Svelte build now routes those calls through a wrapper that hands
the runtime an <code>exclude</code> value satisfying both contracts, so
one published build stays valid across the whole <code>svelte:
">=5 <6"</code> peer range.</p>
</li>
</ul>
<h2><code>@embedpdf/engines</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/models</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/pdfium</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-annotation</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-attachment</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-bookmark</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-capture</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-commands</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-document-manager</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-export</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-form</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/embedpdf/embed-pdf-viewer/blob/v2.15.0/packages/plugin-print/CHANGELOG.md">@embedpdf/plugin-print's
changelog</a>.</em></p>
<blockquote>
<h2>2.15.0</h2>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/embedpdf/embed-pdf-viewer/commit/a8faf04b15291c895118b819d7395c3bca8a4959"><code>a8faf04</code></a>
chore: version packages</li>
<li>See full diff in <a
href="https://github.com/embedpdf/embed-pdf-viewer/commits/v2.15.0/packages/plugin-print">compare
view</a></li>
</ul>
</details>
<br />
Updates `@embedpdf/plugin-redaction` from 2.14.4 to 2.15.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/embedpdf/embed-pdf-viewer/releases">@embedpdf/plugin-redaction's
releases</a>.</em></p>
<blockquote>
<h2>Release v2.15.0</h2>
<h2><code>@embedpdf/plugin-selection</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h3>Minor Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/embedpdf/embed-pdf-viewer/pull/685">#685</a>
by <a href="https://github.com/simonmysun"><code>@simonmysun</code></a>
– Add <code>setSelection(range, documentId?)</code> to the selection
capability and document scope for programmatically applying or restoring
a text selection.</p>
<p>It accepts the same <code>SelectionRangeX</code> (<code>{ start, end
}</code> glyph pointers) shape emitted by
<code>onSelectionChange</code>, so a saved selection can be passed
straight back in to restore it; passing <code>null</code> clears the
selection. Page geometry is loaded on demand, so the returned task
resolves only once the highlight rects are computed. The range is
normalized (start/end may be given in any order), invalid input
(malformed range, non-integer/negative indices, out-of-bounds pages) is
rejected, glyph indices are clamped to the available page geometry, and
previously highlighted pages are repainted so switching to a disjoint
selection no longer leaves stale highlights behind.</p>
</li>
</ul>
<h2><code>@embedpdf/core</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/embedpdf/embed-pdf-viewer/pull/703">#703</a>
by <a
href="https://github.com/berkayozdin"><code>@berkayozdin</code></a> –
Make the precompiled Svelte output work on every Svelte 5 runtime</p>
<p>Svelte 5.56 changed the <code>exclude</code> argument of the private
<code>rest_props</code> runtime helper from an array
(<code>exclude.includes(key)</code>) to a <code>Set</code>
(<code>exclude.has(key)</code>). The <code>*/svelte</code> entry points
ship precompiled component code, so output built against one side of
that change throws on the other: the currently published packages fail
with <code>TypeError: exclude.has is not a function</code> on Svelte
>= 5.56, which aborts the render of every EmbedPDF Svelte
component.</p>
<p>The Svelte build now routes those calls through a wrapper that hands
the runtime an <code>exclude</code> value satisfying both contracts, so
one published build stays valid across the whole <code>svelte:
">=5 <6"</code> peer range.</p>
</li>
</ul>
<h2><code>@embedpdf/engines</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/models</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/pdfium</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-annotation</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-attachment</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-bookmark</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-capture</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-commands</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-document-manager</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-export</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-form</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/embedpdf/embed-pdf-viewer/blob/v2.15.0/packages/plugin-redaction/CHANGELOG.md">@embedpdf/plugin-redaction's
changelog</a>.</em></p>
<blockquote>
<h2>2.15.0</h2>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/embedpdf/embed-pdf-viewer/commit/a8faf04b15291c895118b819d7395c3bca8a4959"><code>a8faf04</code></a>
chore: version packages</li>
<li>See full diff in <a
href="https://github.com/embedpdf/embed-pdf-viewer/commits/v2.15.0/packages/plugin-redaction">compare
view</a></li>
</ul>
</details>
<br />
Updates `@embedpdf/plugin-render` from 2.14.4 to 2.15.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/embedpdf/embed-pdf-viewer/releases">@embedpdf/plugin-render's
releases</a>.</em></p>
<blockquote>
<h2>Release v2.15.0</h2>
<h2><code>@embedpdf/plugin-selection</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h3>Minor Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/embedpdf/embed-pdf-viewer/pull/685">#685</a>
by <a href="https://github.com/simonmysun"><code>@simonmysun</code></a>
– Add <code>setSelection(range, documentId?)</code> to the selection
capability and document scope for programmatically applying or restoring
a text selection.</p>
<p>It accepts the same <code>SelectionRangeX</code> (<code>{ start, end
}</code> glyph pointers) shape emitted by
<code>onSelectionChange</code>, so a saved selection can be passed
straight back in to restore it; passing <code>null</code> clears the
selection. Page geometry is loaded on demand, so the returned task
resolves only once the highlight rects are computed. The range is
normalized (start/end may be given in any order), invalid input
(malformed range, non-integer/negative indices, out-of-bounds pages) is
rejected, glyph indices are clamped to the available page geometry, and
previously highlighted pages are repainted so switching to a disjoint
selection no longer leaves stale highlights behind.</p>
</li>
</ul>
<h2><code>@embedpdf/core</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/embedpdf/embed-pdf-viewer/pull/703">#703</a>
by <a
href="https://github.com/berkayozdin"><code>@berkayozdin</code></a> –
Make the precompiled Svelte output work on every Svelte 5 runtime</p>
<p>Svelte 5.56 changed the <code>exclude</code> argument of the private
<code>rest_props</code> runtime helper from an array
(<code>exclude.includes(key)</code>) to a <code>Set</code>
(<code>exclude.has(key)</code>). The <code>*/svelte</code> entry points
ship precompiled component code, so output built against one side of
that change throws on the other: the currently published packages fail
with <code>TypeError: exclude.has is not a function</code> on Svelte
>= 5.56, which aborts the render of every EmbedPDF Svelte
component.</p>
<p>The Svelte build now routes those calls through a wrapper that hands
the runtime an <code>exclude</code> value satisfying both contracts, so
one published build stays valid across the whole <code>svelte:
">=5 <6"</code> peer range.</p>
</li>
</ul>
<h2><code>@embedpdf/engines</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/models</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/pdfium</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-annotation</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-attachment</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-bookmark</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-capture</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-commands</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-document-manager</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-export</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h2><code>@embedpdf/plugin-form</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/embedpdf/embed-pdf-viewer/blob/v2.15.0/packages/plugin-render/CHANGELOG.md">@embedpdf/plugin-render's
changelog</a>.</em></p>
<blockquote>
<h2>2.15.0</h2>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/embedpdf/embed-pdf-viewer/commit/a8faf04b15291c895118b819d7395c3bca8a4959"><code>a8faf04</code></a>
chore: version packages</li>
<li>See full diff in <a
href="https://github.com/embedpdf/embed-pdf-viewer/commits/v2.15.0/packages/plugin-render">compare
view</a></li>
</ul>
</details>
<br />
Updates `@embedpdf/plugin-rotate` from 2.14.4 to 2.15.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/embedpdf/embed-pdf-viewer/releases">@embedpdf/plugin-rotate's
releases</a>.</em></p>
<blockquote>
<h2>Release v2.15.0</h2>
<h2><code>@embedpdf/plugin-selection</code><a
href="https://github.com/2"><code>@2</code></a>.15.0</h2>
<h3>Minor Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/embedpdf/embed-pdf-viewer/pull/685">#685</a>
by <a href="https://github.com/simonmysun"><code>@simonmysun</code></a>
– Add <code>setSelection(range, documentId?)</code> to the selection
capability and document scope for programmatically applying or restoring
a text selection.</p>
<p>It accepts the same <code>SelectionRangeX</code> (<code>{ star...
_Description has been truncated_
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
# Description of Changes
PR updates built from source dep versions in Dockerfiles
Changes:
* Updated `CALIBRE_VERSION` from 9.4.0 to 9.13.0 in
`docker/base/Dockerfile`.
* Updated `GS_VERSION` (Ghostscript) from 10.06.0 to 10.07.1 in
`docker/base/Dockerfile`.
* Updated `IM_VERSION` (ImageMagick) from 7.1.2-13 to 7.1.2-29 in
`docker/base/Dockerfile`.
* Updated `QPDF_VERSION` is already at 12.3.2, no change.
* Updated `UNOSERVER_VERSION` from 3.6 to 3.7 in both
`docker/base/Dockerfile` and `docker/unoserver/Dockerfile` to align with
the client version and avoid wire mismatches.
* Updated `TASK_VERSION` from 3.49.1 to 3.52.0 in
`docker/embedded/Dockerfile`, `docker/embedded/Dockerfile.fat`,
`docker/embedded/Dockerfile.ultra-lite`, and `engine/Dockerfile`.
<!--
Please provide a summary of the changes, including:
- What was changed
- Why the change was made
- Any challenges encountered
Closes #(issue_number)
-->
---
## 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.
# Description of Changes
Adds a PDF/UA converter, an accessibility report, and PDF/A conformance
level A.
**New: `POST /api/v1/convert/pdf/ua`** (Convert tool, "PDF/UA" target).
Tags an untagged PDF, marks
decorative content as artifacts, embeds missing fonts and applies the
document-level PDF/UA
requirements (title, language, tab order, form-field descriptions), then
validates with veraPDF. The
`pdfuaid` declaration is written only if validation passes, so a
returned file never claims more
than it delivers; response headers report whether it was declared, how
many checks still fail and
how many images still need a description.
**New: `POST /api/v1/security/accessibility-report`.** Reports what
fails, what the converter can fix
on its own, what needs a person, and lists the figures needing a
description with the keys the
conversion accepts back. Read-only; does not modify the file. Capped at
100 MB / 2000 pages and
weighted `LARGE_WEIGHT`, since it runs a full veraPDF pass plus the
converter's layout analysis over
every page.
**PDF/A level A.** `pdfa-1a`, `pdfa-2a` and `pdfa-3a` output formats on
the existing
`/api/v1/convert/pdf/pdfa` endpoint. Level A is level B plus tagging, so
the document is tagged
after Ghostscript (which discards any structure tree it is given) and
the level A claim is written
only if veraPDF agrees. Optional `pdfUa=true` additionally declares
PDF/UA alongside PDF/A, again
only if it validates.
Honesty rules the implementation holds to:
- **Never claim a level that was not reached.** If tagging fails, the
file is returned at level B and
is named `_PDFA-2b.pdf`, not `_PDFA-2a.pdf`. With `strict=true` the
request fails outright rather
than returning a level B file against a level A request, and a level B
pass no longer satisfies a
strict level A request.
- **Never relabel a document's language.** The requested language
(default `en-GB`) is applied only
when the document declares none; a French PDF stays French unless the
caller sets
`overrideLanguage`, and ignoring a requested language is reported as a
warning.
- **Never invent alternative text.** Descriptions come from the caller.
The Convert panel can list
the images needing one (via the report endpoint) and send them back per
figure; any image left
undescribed blocks the conformance claim rather than being papered over.
- **Never certify hidden content.** Marking images decorative, or
suppressing text that could not be
tagged reliably, withdraws the claim instead of passing the checker by
hiding content.
PDF/UA-1 and PDF/UA-2 are both offered; UA-2 raises the file to PDF 2.0
and namespaces the structure
tree, and its test asserts conformance rather than merely reporting it.
Convert steps saved in Automations/Pipelines round-trip their PDF/UA
settings (profile, language,
override, title, font embedding, descriptions).
---
## 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.
Auto-generated by stirlingbot[bot]
This PR updates the backend license report based on dependency changes.
Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
# Description of Changes
This PR bumps the Stirling PDF application version from `2.14.2` to
`2.14.3` across the project.
Changes include:
- Updated the Gradle project version in `build.gradle` to `2.14.3`.
- Updated the Tauri desktop application version in
`frontend/editor/src-tauri/tauri.conf.json`.
- Updated the AUR package version for `stirling-pdf-desktop`.
- Updated the AUR package version for `stirling-pdf-server-bin`.
- Updated the mocked `appVersion` used by the core frontend server
experience simulations.
- Updated the mocked `appVersion` used by the proprietary frontend
server experience simulations.
- Kept all application, desktop, packaging, and test/simulation version
references synchronized for the `2.14.3` release.
The change prepares the project metadata and packaging configuration for
the `2.14.3` release and prevents different components from reporting or
packaging the previous `2.14.2` version.
---
## 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.
## What
The super-search work pinned the editor's `WorkbenchBar` visible on
every view except My Files, even with no file open. That left an empty
workbench showing a fully painted bar whose only live control was the
search — download / close / print / save were all disabled, because
those actions only make sense with a file open.
This stops forcing the bar. When nothing is open, only the global search
floats (unpainted, centered), mirroring how the Processor already works.
When a file **is** open, the `WorkbenchBar` renders exactly as before.
Also fixes a smaller Processor issue: its floating search strip was
shorter than the sidebar logo row, so the search sat higher than the
brand. Its height now matches the logo row (51px) so they line up.
## Changes
- **`Workbench.tsx`** — render the `WorkbenchBar` only when a file is
open (or a custom view supplies content); otherwise render the new
floating search. My Files and `hideTopControls` custom views are
unchanged.
- **`WorkbenchFloatingSearch.tsx` / `.css`** (new) — the editor's
`SuperSearch` floated in an unpainted strip, mirroring
`PortalSearchBar`. Its vertical band matches the bar's so opening a file
swaps in the bar without a shift.
- **`PortalSearchBar.css`** — strip height matched to
`.portal-sidebar__logo` (51px) so the Processor search aligns with the
logo.
The notification bell is intentionally out of scope — it ships in a
separate PR.
## Before / after
(ignore the bell icon in the after that’s not live yet)
<img width="2056" height="1077" alt="Screenshot 2026-08-20 at 2 28
17 AM"
src="https://github.com/user-attachments/assets/144f5216-4784-42b2-8c09-afda43577ad0"
/>
<img width="2056" height="1071" alt="Screenshot 2026-08-20 at 2 28
31 AM"
src="https://github.com/user-attachments/assets/63af9303-af8a-48dd-b113-485169fb4924"
/>
- **Editor, no file:** painted bar with disabled buttons → just a
floating search.
- **Editor, file open:** unchanged.
- **Processor:** search now vertically aligned with the logo.
## Testing
- `task frontend:check` — lint (incl. colour linters) + typecheck +
tests (247 files / 2137 tests) all pass.
- Processor alignment verified in Storybook (`Portal/Shell/AppShell`):
logo row, search strip, and search pill share the same vertical center.
- Editor float not verified in-browser (local backend is behind a login
gate); covered by types/tests and reuses the verified Processor pattern.
# Description of Changes
Currently in the Processor's Pipelines page, none of the tools which
require supporting files are usable because it's never been hooked up to
the new API to upload supporting files. This PR hooks it up to that so
all tools using supporting files work in the processor. I had to tweak
the type generation a little for this so we have a static map of which
params are for supporting files so we know to handle them differently.
The `Test with a file` button has to work a little differently than the
main run since it's running an ad-hoc pipeline so the files haven't
necessarily been saved to the server yet. In this case, it'll use
whatever local changes the user has made for those pipeline steps, and
for all other steps, it'll just use what's saved in the server.
Auto-generated by stirlingbot[bot]
This PR updates the frontend license report based on changes to
package.json dependencies.
Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
# Description of Changes
This pull request upgrades the frontend PDF dependency from
`@cantoo/pdf-lib` 2.6.5 to 2.8.2.
- Updated `frontend/package.json` to require `@cantoo/pdf-lib` `^2.8.2`.
- Regenerated `frontend/package-lock.json` with `@cantoo/pdf-lib@2.8.2`,
`pako@2.2.0`, and `node-html-better-parser@1.5.9`.
- Added the root npm `pako` override recommended by the upstream
release.
- The upgrade brings upstream parser, object-stream, encryption, form,
PNG, and PDF serialization fixes into the frontend dependency.
- No application API migration was required because the project does not
use the newly added PDF/A, XFA, Factur-X, incremental-update, fontkit,
or page-content-extraction APIs.
The main challenge was validating the broad upstream change set against
the project's actual usage. The frontend typecheck and a direct PDF
create/save/load smoke test passed. The complete `frontend:check` and
`frontend:test` tasks exceeded the available execution timeout without
reporting a test failure.
No related issue.
---
## 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/HowToAddNewLanguage.md)
(if applicable)
- [x] 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/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)
### 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 tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing)
for more details.
## The problem
The `dev` profile hardcoded one project ref (`qacaivhsjtftfwtgjvva`) in
five places: the ref, the Supabase URL, the publishable key, the
datasource host and the meter endpoint.
That made it both the shared environment everyone relies on *and* the
only thing you could point the backend at. Testing an open SaaS PR meant
hand-overriding all five via env just to reach that PR's Supabase
preview branch, which is the only place the PR's migrations have
actually been applied. Get it wrong and you see `relation
"stirling_pdf.<new table>" does not exist` for a table the PR added,
which is what happened on
[#7414](https://github.com/Stirling-Tools/Stirling-PDF/pull/7414).
## One task per environment
```bash
task dev:saas # backend + frontend + engine, against this PR's preview branch
task staging:saas # backend + frontend + engine, against the shared v3 project
task backend:dev:saas # backend only, preview branch
task backend:staging:saas # backend only, v3
```
| | how | vars | project |
|---|---|---|---|
| prod | `PROFILES=none` | `SAAS_DB_*` | the live one |
| staging | `PROFILES=staging` | `SAAS_STAGING_*` | pinned to v3, always
there |
| dev | `PROFILES=dev` | `SAAS_DEV_*` | follows a SaaS PR's preview
branch |
`PROFILES` is still the underlying switch, so the old spelling keeps
working. Production deliberately has no named task: reaching it should
take a conscious `PROFILES=none`, not a tab-complete.
**staging** is the old `dev` configuration, moved and kept pinned. The
value of a shared environment is that it is still there tomorrow:
reproduce a bug, paste a link to a colleague, share data.
**dev** is parameterised by `SAAS_DEV_PROJECT_REF` and derives the
Supabase URL, JWT issuer, JWKS, meter endpoint and (unless overridden)
the database host from it. Switching which PR you are testing is one
variable instead of five. With no ref set, `task backend:dev:saas` stops
and says what to set rather than falling back.
## The frontend was the real gap
`frontend/editor/.env` is committed and pins the **production** Supabase
project, and nothing in the frontend knew about dev or staging. So `task
dev:saas` gave you a backend on a preview branch and a login against
prod, unless you happened to have hand-written
`frontend/editor/.env.saas.local`.
The dev tasks now read the backend's env files and derive
`VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY` from the
same project ref the backend resolved, so the two halves cannot point at
different projects. Nothing to keep in sync by hand, no new vite mode,
and `SAAS_ENV=prod` opts back out to the committed values.
## Where to put your local values
Two files, both gitignored, neither ever committed:
**`app/.env.saas.local`** is the only one you normally need. The tasks
load it for the backend *and* the frontend.
```bash
# staging: everything else is already defaulted, so this is all it takes
SAAS_STAGING_DB_PASSWORD=...
# dev: the preview branch of the PR you are testing, from its "Supabase Preview" check.
# A branch has its OWN password and API keys; the parent project's will not authenticate.
SAAS_DEV_PROJECT_REF=...
SAAS_DEV_DB_PASSWORD=...
SAAS_DEV_PUBLISHABLE_KEY=...
# prod, if you ever need it
SAAS_DB_PROJECT_REF=...
SAAS_DB_URL=...
SAAS_DB_PASSWORD=...
SUPABASE_EDGE_FUNCTION_SECRET=...
```
**`frontend/editor/.env.saas.local`** is no longer needed for choosing a
Supabase project, and is best left empty or deleted. If you have one
from before this PR, note that the task-supplied values now win, which
is the point: the frontend follows the backend.
**A blank is not the same as absent.** A dotenv line with an empty value
still *sets* the variable, and Spring's `${VAR:default}` only falls back
when a variable is absent. So `.env.saas` lists what you must set as
blanks, and leaves out the two `*_DB_URL` overrides, which have real
defaults to fall back to. This is not theoretical, see below.
Committed `app/.env.saas` holds non-secret defaults only. Real secrets
are passwords, the edge-function secret and service-role keys. Project
refs and publishable keys are neither: a ref is the public
`<ref>.supabase.co` subdomain and a publishable key ships in the browser
bundle by design, which is why `frontend/editor/.env` has always carried
prod's.
## Three bugs found while building the tasks
All three were in this PR's own earlier commits, and all three were
caught by actually booting things rather than by reading the config.
**staging could not boot at all.** A blank `SAAS_STAGING_DB_URL=` in
`.env.saas` set the variable to empty, so
`${SAAS_STAGING_DB_URL:jdbc:...}` resolved to `""` and startup failed
with `spring.datasource.url is required when the saas profile is
active`. The file already carried a comment warning about exactly this;
it had only been applied to the dev block. The original verification for
this PR was "placeholders resolve" and "the task parses", neither of
which boots anything.
**The dev to staging fallback ran `ddl-auto=update` against shared v3.**
The dev profile sets `update`, which is right for a disposable preview
branch, and separately fell back to staging's project ref. Together that
meant Hibernate was free to reconcile tables that RLS policies depend
on. `application-staging.properties` pins `none`, but that only applies
when the staging profile is the active one, which it was not on the
fallback path. There is no fallback now: with no ref the task stops
before gradle, and the frontend fails the same way, both naming the
variable.
**`PROFILES=` never selected production.** Go template `default` treats
`""` as absent, so it silently resolved back to `dev`. It is
`PROFILES=none` now.
## Two choices worth reviewing
**Staging keeps its committed project ref**, now as a
`${SAAS_STAGING_PROJECT_REF:...}` default in one place, with the URL,
database host and meter endpoint all derived from it. So staging still
works with zero setup, and repointing it is one variable. Nothing in CI
referenced the ref or the profile. Its publishable key default carries
no inline `gitleaks:allow`: a trailing comment in a `.properties` file
is part of the value, so the pragma ended up inside the key. It is in
`.gitleaksignore` instead.
**`SAAS_DEV_DB_URL` still overrides the whole URL**, so a branch needing
the pooler host rather than the direct one is reachable without touching
committed config.
## Verification
- `task backend:staging:saas` boots against v3 and serves `200`. It
could not boot before this commit.
- `task backend:dev:saas` with no ref stops before gradle naming the
variable, and `PROFILES=none` still reaches production. `task
frontend:dev:saas` fails the same way; `SAAS_ENV=staging` still resolves
with no local config.
- Frontend routing picks the SaaS runner for dev/staging and the plain
runner for prod; the derivation returns the right URL and key for each.
- Vite's `process.env` precedence and Task's dotenv/env semantics were
measured, not assumed. That is how one trap surfaced: Task sets an
`env:` key even when its value resolves to empty, and Vite treats an
empty `process.env` `VITE_*` as authoritative over a committed `.env`.
Putting the Supabase vars on the shared `dev:_run` would have blanked
Supabase config for the core, proprietary and desktop dev servers, so
the SaaS path has its own runner.
- `:saas:spotlessApply` and `:saas:compileJava` green.
`DevProfileProjectNotice` becomes `SaasProjectNotice` and covers both
profiles, stating the project ref and `ddl-auto` at startup so which
environment you are on is never a guess.
No behaviour change for prod: the `saas` profile is untouched.
## What
Running a stored policy against its **configured sources** (`POST
/api/v1/policies/{id}/trigger`, the manual "run now") now requires the
policy-management role — global admin self-hosted, team leader on SaaS —
alongside the existing team scoping.
## Why
A source sweep operates on the team's configured sources using the
server's stored connection credentials, so it belongs with the other
policy-management capabilities rather than with ordinary use. Team
scoping on its own didn't express that distinction.
## Not changed
- `POST /{id}/run` — running a policy over documents the **caller
supplied** stays open to every team member. That's ordinary editor
enforcement on upload and export, and gating it would break it.
- Ad-hoc pipelines (`/run`, `/run/stream`).
- The scheduled, folder-watch and webhook triggers.
- Single-user deployments (login disabled), which have no roles.
## Implementation
`PolicyManagementAuthority` gains `canTriggerPolicies()`, kept separate
from `canEditPolicies()` so the two capabilities can diverge later. Both
current implementations grant it to the same principals that may edit
policies.
## Tests
- role absent → 403, rejected before any run starts
- role present → 202
- login disabled → check skipped entirely
- `/{id}/run` asserted to consult neither authority method, so the gate
can't quietly extend to the editor path later
## Summary
This pull request restructures Gradle dependency caching across the
GitHub Actions workflows.
The central `gradle-cache-prime` job is responsible for preparing the
shared backend Gradle cache. Reusable workflows restore that shared
cache without writing to the same key, while independently triggered
workflows use isolated cache namespaces.
## What changed
### Shared Gradle cache
- Added a stable `gradle-v1-` cache namespace for the shared backend
cache.
- The cache key includes the runner OS, runner architecture, JDK
version, and the relevant Gradle configuration files.
- The cache key is calculated before Gradle runs and reused for the
later save step.
- The prime job performs a lookup first and resolves backend
dependencies only when the exact cache is missing.
- This prevents Gradle or Spotless changes during the prime step from
producing a different save key from the key used by downstream jobs.
### Reusable workflows
- Backend, OpenAPI, license, Docker, E2E, and migration workflows
restore the shared cache instead of writing to the shared key.
- The backend build matrix includes `matrix.jdk-version` in its cache
key.
- Enterprise, Tauri, and generated-model workflows support the
`use_shared_cache` boolean input.
- When `use_shared_cache` is enabled, those workflows restore the shared
cache.
- When it is disabled, they use workflow-specific cache namespaces.
### Independent workflows
Independent workflows now use separate cache prefixes, including:
- `gradle-license-report-v1-`
- `gradle-swagger-v1-`
- `gradle-push-docker-v1-`
- `gradle-tauri-releases-v1-`
- `gradle-deploy-pr-v1-`
- `gradle-playwright-e2e-v1-`
- `gradle-generated-models-v1-`
This prevents them from creating or affecting the shared backend cache
before the prime job.
### Build and E2E flow
- Removed the `-PnoSpotless` option from the central Gradle
dependency-resolution command.
- Removed the separate Gradle dependency prime/retry logic from the live
E2E workflow.
- Connected the Tauri build and generated-models check to the central
cache-prime job.
## Motivation
Previously, multiple workflows could use and save the same Gradle cache
key independently. The first workflow to save the cache could therefore
determine its contents, even if it had resolved a different or
incomplete set of dependencies.
The cache key was also evaluated after some Gradle tasks had run. If
Gradle or Spotless modified a file covered by `hashFiles(...)`, the save
key could differ from the restore key used by downstream jobs.
This change gives the shared cache a single owner, isolates
workflow-specific caches, and makes cache usage deterministic across the
CI pipeline.
## Expected result
- `gradle-cache-prime` is the single writer for the shared backend
Gradle cache.
- Downstream jobs restore the same cache without competing cache writes.
- Independently triggered workflows remain isolated through their own
cache namespaces.
- Changes to the monitored Gradle configuration files produce a new
cache key.
- The normal Gradle/Spotless path is included when the shared cache is
populated.
## Validation
- Compared the cache key expressions and `hashFiles(...)` inputs across
the affected workflows.
- Verified that the central restore and save steps use the same
precomputed key.
- CI should confirm that the prime job populates the shared cache and
downstream workflows only restore it.
## Checklist
- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have performed a self-review of my changes
- [ ] I have run the relevant CI checks
- [ ] I have tested the workflow changes
## What
Both sidebars ended in a different bottom section. The editor showed an
account row (avatar, name, settings); the processor showed a "Link
Stirling account" CTA plus a `Settings` nav item and no identity at all.
They are now **one shared `<NavFooter>`** rendering the same rows in
both apps, in this order:
1. the link-account CTA (self-hosted, when unlinked)
2. free credits remaining
3. **Open \<the other app\>**
4. the account row — avatar, name, settings
It's a **single surface** with hairline dividers between rows, not
stacked cards. Rows are assembled as a list, so a row this build doesn't
show (no wallet, no processor access, nothing to link) takes its divider
with it rather than leaving a stray line.
This also fixes the profile-picture/initials desync between the sidebar
and the account settings page.
## Screenshots
Captured with the stubbed Playwright harness at 1600x900, scoped to the
sidebar and auto-cropped to the region that actually changed. Base is
`origin/main`; every state is driven by dummy backend stubs so all the
nav-bar permutations are covered.
<img width="2104" height="3044" alt="montage_cloud-dark"
src="https://github.com/user-attachments/assets/667c9a71-3ab7-4582-9259-08cda521e238"
/>
<img width="2104" height="3044" alt="montage_cloud-light"
src="https://github.com/user-attachments/assets/126bc994-efa3-436b-bf50-31d76f574eaa"
/>
<img width="2104" height="1492" alt="montage_editor-dark"
src="https://github.com/user-attachments/assets/c725726d-ea90-4fa9-bf0a-6014f3729869"
/>
<img width="2104" height="1490" alt="montage_editor-light"
src="https://github.com/user-attachments/assets/d0d34e0a-ac60-4f8c-af15-afb66fbd679e"
/>
<img width="2104" height="1066" alt="montage_processor-dark"
src="https://github.com/user-attachments/assets/51ccfe63-c327-41f4-ab91-74555b6f7148"
/>
<img width="2104" height="1068" alt="montage_processor-light"
src="https://github.com/user-attachments/assets/c90d7aba-90fa-4998-ac8f-26ce666dde71"
/>
The free-credits meter is a cloud-build surface, so the self-hosted
capture can't reach it. Those states come from the new Storybook stories
with dummy wallet data (`Shared/NavFooter`), which is also where the
credit tone bands and the collapsed rail are easiest to review.
## How it's wired
`NavFooter` is purely presentational. Each app resolves its own data
through three `@app/*` seams, so core carries no build-specific gating
and any box whose data is absent is dropped rather than rendered empty.
| Seam | core | cloud / proprietary / saas |
|---|---|---|
| `useFreeCreditsSummary` | `null` — self-hosted editor installs aren't
metered | cloud reads `freeRemaining` / `freeAllowance` off the same
`useWallet()` the Plan page's free meter uses, so the sidebar and Plan
can't disagree |
| `useOtherAppSwitch` | `null` — core ships no processor | gated on
`portalAccess` (`/api/v1/auth/me` in SaaS, the Spring session flag
self-hosted) |
| Link-account CTA | n/a | unchanged conditions — passed in as
`accountExtras`, still only when `linkState === "unlinked"`, still a
no-op in SaaS |
- The processor reads the meter through its own
`@portal/hooks/useFreeCreditsSummary` rather than the editor's `@app`
one. Self-hosted resolves `@app/*` as proprietary → core, where the
cloud wallet hook isn't in the cascade, and the implementation can't
live in `proprietary/` because core/desktop builds ship no portal and
must never resolve `@portal`. Keeping it in `portal/` gets the figure to
the linked self-hosted processor without weakening that rule; it reads
the same `GET /api/v1/payg/wallet` the Usage page's trial meter already
renders, gated on link state and behind the portal's query cache.
`portal-saas/` just re-exports the cloud hook, so both footers share one
fetch.
The processor-access gate previously lived in two near-identical
`AppSwitcher` copies. It moves into `useOtherAppSwitch`, `AppSwitcher`
now reads it too, and the duplicate
`saas/components/shared/AppSwitcher.tsx` is deleted — the logo switcher
and the footer row can no longer disagree about access.
## Profile picture sync
One `useAccountIdentity` hook now backs the editor footer, the processor
footer and the account settings page. Previously settings derived its
initial from `email[0]` while the sidebar used `displayName[0]`, and the
two drew different blue discs. Alongside that, the shared `Avatar`:
- falls back to initials when a picture URL fails to load, instead of
leaving an empty disc
- renders one letter for single-word names (`admin` → "A", not "AD")
- gains an `xl` size so the settings hero disc is the same component
## Notes
- Labelled **"Free credits"** rather than "free monthly credits":
`freeAllowance` is documented as a one-time lifetime grant, not a
monthly reset, so "monthly" would misdescribe the data. Happy to change
if the backend semantics differ from the type comments.
## Testing
- `task frontend:check` and `task frontend:typecheck:all` pass (all 9
build variants).
- 9 new `Shared/NavFooter` stories pass the Chromium + axe story scan;
`frontend:storybook:a11y:changed` reports no regressions.
- Stubbed E2E suite passes, including the `config-button` tour/settings
specs that target the account row. Two failures (`console-clean ›
landing`, `viewer-text-selection › Ctrl+C`) also fail on `origin/main`
locally — they need a backend on :8080 and clipboard permissions.
Every top bar styled itself, so none of them matched the new UI. Also,
colors on the premium banner (and possibly others) clashed since the
theme changes.
## Before Example Issue
<img width="1934" height="348" alt="Screenshot 2026-08-17 at 11 47
20 PM"
src="https://github.com/user-attachments/assets/b6f13207-2f47-4084-bd3b-2392f572c1a1"
/>
## After (all)
<img width="2880" height="800" alt="danger__dark"
src="https://github.com/user-attachments/assets/6b311dec-23e7-4059-a6bb-75527cbd2e34"
/>
<img width="2880" height="800" alt="danger__light"
src="https://github.com/user-attachments/assets/3b04b109-5146-4874-88b3-d99770ea51f8"
/>
<img width="2880" height="800" alt="default-app__dark"
src="https://github.com/user-attachments/assets/b0101b98-fce8-467a-99a6-dd40b8864da1"
/>
<img width="2880" height="800" alt="default-app__light"
src="https://github.com/user-attachments/assets/8aa21f01-6549-4d78-b217-c5547d60ab5b"
/>
<img width="2880" height="800" alt="free-tier-limit__dark"
src="https://github.com/user-attachments/assets/07fd7498-f44f-408f-8c79-9b5ea55e13df"
/>
<img width="2880" height="800" alt="free-tier-limit__light"
src="https://github.com/user-attachments/assets/f38c527b-9f64-4db0-b84c-56ca48e464cc"
/>
<img width="2880" height="800" alt="server-attention__dark"
src="https://github.com/user-attachments/assets/447a36fa-056d-4ca9-8b30-04aa0ccd6ed1"
/>
<img width="2880" height="800" alt="server-attention__light"
src="https://github.com/user-attachments/assets/f0311d45-a218-4846-b0ac-47996e2637c5"
/>
<img width="2880" height="800" alt="team-invitation__dark"
src="https://github.com/user-attachments/assets/cc368473-3b9f-4ab0-878a-67da993874c2"
/>
<img width="2880" height="800" alt="team-invitation__light"
src="https://github.com/user-attachments/assets/19d52a3e-4028-46f0-8562-9bd9cef9397a"
/>
<img width="2880" height="800" alt="upgrade-prompt__dark"
src="https://github.com/user-attachments/assets/e9d20daa-f41f-46d9-b827-a84f276e8af1"
/>
<img width="2880" height="800" alt="upgrade-prompt__light"
src="https://github.com/user-attachments/assets/9b6250c1-6a61-40d6-a7ab-e37398d67322"
/>
## What changed
- `InfoBanner` exposed 8 colour-override props (`background`,
`borderColor`, `textColor`, `iconColor`, `buttonColor`,
`buttonTextColor`, `closeIconColor`, `buttonVariant`), so every caller
invented its own look. Replaced with a closed tone set: `info` · `promo`
· `warning` · `danger`.
- Tone drives the whole bar — fill, border, icon and the button — so a
CTA can't drift from the bar it sits on. Text is neutral in every tone;
only the icon carries the tone colour.
- All colour comes from `--c-*` tokens mixed over `--c-surface`, so the
bars follow light and dark instead of ignoring them. The old bars were
hardcoded: in dark mode the two licence warnings stayed cream-on-white.
- `promo` keeps the gradient it was always meant to have, built from the
existing `--c-hue-indigo`/`--c-hue-purple` stops (documented in
`colors.css` as gradient hues, deliberately not accent-following), with
the existing `premium` button accent on it.
- Deleted the hardcoded colours from all four callers: the purple
gradient (`#667eea`→`#764ba2`), the orange soup (`#FFF4E6` / `#9A3412` /
`#EA580C`) duplicated across the urgent banner and the admin plan
section, and the fixed dark bar (`--mantine-color-dark-7`) on the team
invitation.
- `UpgradeBanner|AdminPlanSection` sat on the theme linter's exemption
list, which is how those colours survived the theme migration. Exemption
removed, so `code-colors` now guards them.
- The banner's class was colliding with `core/ui/Banner.css`'s
`.sui-banner` (16 live rules), which restyled it in the app but not in
Storybook — that's why the two disagreed on radius, border and tone.
Renamed to `.app-banner`; the two surfaces now render identically.
- Bar is square and full-bleed with a single hairline rule underneath;
button labels are optically centred.
- Added `--c-warning-subtle`, matching the existing `--c-danger-subtle`
/ `--c-success-subtle`.
- New `Shared → Top bars` story renders all six bars at once, so a
change to the shared component is visible against the whole set.
- Unrelated one-liner: `frontend/.prettierignore` now ignores the
gitignored `editor/screenshots/` capture artifacts, which were failing
`format:check` locally. Happy to drop it if you'd rather keep this PR to
the bars.
## Testing
- `task frontend:check` — typecheck, lint (oxlint + 4 theme-lint passes
+ stylelint), format, 244 files / 2119 tests.
- `frontend:storybook:a11y:changed` — clean in light and dark.
- The a11y gate caught a real defect mid-change: giving each banner
`role="region"` with the same label produced duplicate landmarks, which
the app hits for real whenever two banners show at once. Landmark
removed.
- All six bars captured in the running editor, light and dark, and
diffed against `origin/main`'s component rendered with each caller's
original props.
# Description of Changes
Overlay PDFs and Change Metadata both crashed in the Processor because
they required `FilesModalContext` and `ViewerContext` respectively.
Neither of those contexts make sense to provide in the Processor because
there are no files in context and there is no Viewer, so redesign both
tool settings to only optionally require these contexts. Their behaviour
is unchanged in the Editor but they now work in the Processor (just
without the extra info about the active files, since there are none).
Also hooks up the Reorganise Pages settings so that it can be used from
Automate. The component already existed but just wasn't being used,
which just looks like an oversight.
## The problem
The SaaS database has two writers and always has: the Supabase
migrations in the SaaS repo, and Hibernate's `ddl-auto`. That was a
convention rather than a rule, and it leaked twice.
- An older `ddl-auto` run widened `team_memberships.role` to
varchar(255), which needed [a dedicated
migration](https://github.com/Stirling-Tools/Stirling-PDF-SaaS/blob/v3/supabase/migrations/20260804000000_fix_team_memberships_role_varchar50.sql)
to repair, because RLS policies depended on the column.
- `payg_instance_usage` shipped with an entity and **no migration**, and
nobody noticed for months — staging already had the table from an
earlier `ddl-auto` run. It surfaced only when a fresh preview branch,
built from migrations alone, threw `relation does not exist`.
Both are the same bug: nobody had to *say* who owned a table, so the
answer got decided by accident.
## The fix
`SaasSchemaOwnership` is the register — **29 migration-owned, 29
inherited** and left to Hibernate.
`MigrationOwnedSchemaFilter` applies it via Hibernate's
`hbm2ddl.schema_filter_provider`, wired on the **saas profile only**.
Hibernate is never shown a migration-owned table, so it cannot create,
alter, drop or truncate one whatever `ddl-auto` is set to. Inherited
tables stay managed, so a fresh preview branch still heals itself on
first boot. Self-hosted is untouched — there Hibernate rightly owns
everything.
**Why a filter rather than just `ddl-auto=none`:** off, and a fresh
branch is missing the 29 inherited tables. On, and Hibernate can reach
the other 29. The filter is what lets both be true at once.
**Why per-table, not per-schema:** Hibernate's schema management runs
over every mapped entity regardless of namespace. Moving SaaS tables to
their own schema would *not* by itself keep Hibernate out of them —
worth knowing, because that was the intuitive fix and it doesn't work.
## The part that makes it stick
`SaasSchemaOwnershipTest` makes the register binding: every `@Entity` on
the SaaS classpath must appear in exactly one set, so **a new entity
fails the build until someone states who owns its table**. That's the
forcing function that would have caught `payg_instance_usage`.
I verified it bites rather than assuming it — removing a single entry
fails with:
```
These entity tables are not declared in SaasSchemaOwnership, so nobody owns them.
Offending tables -> entities: [policies (stirling.software.proprietary.policy.store.PolicyEntity)]
```
naming both the table and the class, which is what the next person
actually needs.
## One debatable call
The **validate** filter excludes them too. Letting validation through
would flag drift, which is genuinely useful — but `ddl-auto=validate`
fails startup, and it would fail on differences we've deliberately
accepted (`ai_create_sessions` carries columns from a reverted Typst
feature that nothing maps). A boot failure over a table we chose not to
manage is noise. Argued in the javadoc; happy to flip it if you'd rather
have the signal.
## Dependency
Depends on
[Stirling-PDF-SaaS#324](https://github.com/Stirling-Tools/Stirling-PDF-SaaS/pull/324),
which adds migrations for the four SaaS-owned tables that had none.
They're listed here as migration-owned on that basis, so #324 should
land first.
Companion to
[#7483](https://github.com/Stirling-Tools/Stirling-PDF/pull/7483)
(dev/staging profiles with per-profile `ddl-auto`).
## Verification
`:saas:test` green including the 5 new tests, `spotlessCheck` green, and
the mutation check above.
Review Flow PR 3. Stacked on #7296. A recorded failure becomes readable
by the person who caused it.
## What changes
Before this, reading or triaging a failure required leader permissions:
`FileRunEventController.requireFailureReviewAllowed()` returned 403 to
anyone who could not edit policies. #7296 lets any user report a
failure, so they could file into a queue they could never read.
That gate is removed from the endpoints and the decision moves into
`FileRunEventService`:
| Caller | Reads and closes |
|---|---|
| Team leader or admin | the whole team's failures (unchanged) |
| Anyone else | only failures where `actor` is them |
| Team unresolvable | nothing |
| Name unresolvable | nothing |
`GET /kinds` is also opened. It returns static enum metadata, and a
member needs it to render failures they can already see.
## Additions
- An `actor` predicate on both list queries in `FileRunEventRepository`,
threaded through `FileRunEventStore.list`.
- `ReadScope` (permitted, teamId, actor) replacing `TeamScope`, with
`wholeTeam` / `mine` / `denied` factories.
- An actor filter on `dispatch`, so acting on another person's row
answers **404, not 403** — the same response as an id that does not
exist.
## Fixes
- **`report()` filed rows under the wrong team.** It took the team from
the read scope, which returns null for a caller who cannot be named, so
such a report landed unteamed in the bucket every team shares. It now
uses a dedicated `currentTeamId()`.
- **`forgetFiles` narrows to the caller even for a leader.** File ids
are minted by each client, so scoping on team alone would let one caller
close a colleague's incidents by naming ids.
- The controller no longer injects `PolicyManagementAuthority` or
`ApplicationProperties`; with the gate gone it decides nothing.
## Team isolation
Unchanged and covered by database-backed tests rather than mocks.
`FileRunEventStoreDbTest` asserts that a caller with a team sees only
their own team's rows and never the unteamed ones, and that the actor
predicate narrows within a team without ever widening across one. Delete
either clause from the JPQL and one of those tests fails.
No endpoint accepts a team parameter; the team always comes from the
authenticated principal.
**Attribution is fixed here too, because this PR depends on it.** A
failure's actor was read from the MDC audit principal, which carries the
BILLING identity — for a stored policy, always its owner. Since reads
are now narrowed to the rows you are the actor on, a wrong actor means
the member who caused a failure and holds the document reads nothing,
while the policy owner is handed incidents from runs they never
triggered. The triggering user is now carried on the run, separate from
the billing principal and the output owner, and is null for a
trigger-fired sweep so an unattended failure stays ownerless.
`PolicyFailureAttributionTest` runs the real engine, recorder, store and
service together. The two sides used to assert independently — the
engine's test matched the actor with `any()`, which is how this went
unnoticed.
## How to test
Needs a proprietary or SaaS build with login enabled and two accounts in
the same team, one a leader and one not. `task dev:all` gives you the
stack.
1. **As the member**, fail a tool: open a PDF and run **Remove
Password** with a wrong password.
2. **Still as the member**, go to `/processor/documents` → **Failures**.
Before this PR you got nothing here. Now you see your own row, and only
yours.
3. **As the leader**, open the same view. You see the whole team's rows,
including the member's.
4. **Member cannot reach a colleague's row.** As the leader, copy a
row's id from **Show raw JSON**. As the member, `POST
/api/v1/file-run-events/{thatId}/actions/DISMISS`. It answers **404**,
and the row is untouched — it must not answer 403, which would confirm
the row exists.
5. **Member can close their own.** Dismiss your own row as the member.
It leaves the default view.
6. **Deleting a file only closes your own rows.** As the leader, delete
a file in your editor. The member's incidents are untouched even if the
leader's client happened to name the same ids.
## Migration
None. `actor` is an existing column; this only adds predicates to
existing queries.
Bumps `awsSdkVersion` from 2.51.2 to 2.51.3.
Updates `software.amazon.awssdk:s3` from 2.51.2 to 2.51.3
Updates `software.amazon.awssdk:url-connection-client` from 2.51.2 to
2.51.3
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>
Bumps the eclipse-temurin group with 1 update in the /docker/backend
directory: eclipse-temurin.
Bumps the eclipse-temurin group with 1 update in the /docker/base
directory: eclipse-temurin.
Bumps the eclipse-temurin group with 1 update in the /docker/embedded
directory: eclipse-temurin.
Updates `eclipse-temurin` from `2f1da10` to `fbcf915`
Updates `eclipse-temurin` from `2f1da10` to `fbcf915`
Updates `eclipse-temurin` from `2f1da10` to `fbcf915`
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps `awsSdkVersion` from 2.44.12 to 2.51.2.
Updates `software.amazon.awssdk:s3` from 2.44.12 to 2.51.2
Updates `software.amazon.awssdk:url-connection-client` from 2.44.12 to
2.51.2
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps `logback` from 1.5.32 to 1.6.1.
Updates `ch.qos.logback:logback-core` from 1.5.32 to 1.6.1
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/qos-ch/logback/releases">ch.qos.logback:logback-core's
releases</a>.</em></p>
<blockquote>
<h2>Logback 1.6.1</h2>
<p><strong>2026-07-28 Release of logback version 1.6.1</strong></p>
<p>• In TimeBasedRollingPolicy, when the file option is set, the
intermediate file renamed before asynchronous compression now receives
the target archive name without the compression suffix (e.g.
<code>.gz</code>, <code>.zip</code>, <code>.xz</code>). Previously it
used a nanotime-based <code>.tmp</code> suffix. This makes the file
easier to identify if compression fails during rollover. (See also the
following paragraph.)</p>
<p>• On GZ, ZIP, or XZ compression failure, the original (uncompressed)
log file is no longer deleted. Compression strategies now delete the
source file only after successful compression and emit a warning that
the original was left intact.</p>
<p>• ConsoleAppender with <!-- raw HTML omitted --> now probes JLine's
org.jline.jansi.AnsiConsole first and falls back to the legacy
FuseSource org.fusesource.jansi.AnsiConsole class. This keeps ANSI
coloring working after Jansi moved under the JLine project. The optional
org.jline:jansi-core artifact is declared as a dependency alongside the
existing FuseSource jansi dependency. A preferredJansiClassName property
was added for tests. This issue was reported in <a
href="https://redirect.github.com/qos-ch/logback/issues/1043">issues/1043</a>
by <a href="https://github.com/seonwooj0810">seonwoo_jung</a> who also
provided the relevant PR.</p>
<p>• LayoutWrappingEncoder now reports an error at start() when no
layout is set and guards encode() against a null layout. Previously, a
missing layout (for example after an ignored <!-- raw HTML omitted
-->/<!-- raw HTML omitted -->/<!-- raw HTML omitted --> branch) allowed
the encoder to start and then fail with a NullPointerException on every
event, resulting in silent log loss. This issue was reported in <a
href="https://redirect.github.com/qos-ch/logback/issues/1046">issues/1046</a>
by <a href="https://github.com/seonwooj0810">seonwoo_jung</a> who also
provided the relevant PR.</p>
<p>• FileCollisionAnalyser now detects file collisions involving nested
appenders of SiftingAppender. When the nested file or fileNamePattern
does not textually reference the discriminator key (e.g. ${userId}), a
warning is issued at configuration time naming the appender, the key,
and the shared target. This closes a gap where statically declared file
appenders were checked but sifted nested appenders were not. This
enhancement was contributed in [PR <a
href="https://redirect.github.com/qos-ch/logback/issues/1041">#1041</a>](<a
href="https://redirect.github.com/qos-ch/logback/issues/1041">qos-ch/logback#1041</a>)
by <a href="https://github.com/seonwooj0810">seonwoo_jung</a>.</p>
<p>• More defensive handling in SyslogOutputStream and
SyslogAppenderBase: the close() method now ensures that resources are
closed, writes and flushes check that the underlying resources are in a
valid state and fallback to no-op otherwise.</p>
<p>• A bit-wise identical binary of this version can be reproduced by
building from source code at commit
57759f433000a133088ef0441038963134437fbd associated with the tag
v_1.6.1. The release was built using Java "21" 2023-10-17 LTS
build 21.0.1.+12-LTS-29 under Linux Debian 11.6.</p>
<p>• See <a
href="https://logback.qos.ch/news.html#1.6.1">https://logback.qos.ch/news.html#1.6.1</a>
for the original text.</p>
<h2>Logback 1.6.0</h2>
<p><strong>2026-07-23 Release of logback version 1.6.0</strong></p>
<p>• Removed certain deprecated variables, methods, and classes. For the
list of removed members see <a
href="https://logback.qos.ch/notes/release_1.6.0.txt">release_1.6.0.txt.</a></p>
<p>• In <code>AsyncAppenderBase</code>, the
<code>put(ILoggingEvent)</code> method now has the protected modifier to
allow access from derived classes. This change was requested by Thomas
Skjølberg in <a
href="https://redirect.github.com/qos-ch/logback/pull/1053">pr#1053</a>.</p>
<p>• Bump SLF4J dependency to version 2.0.18.</p>
<p>• <strong>See also the overview of the <a
href="https://logback.qos.ch/news.html#latest_stable">1.6.x
series</a>.</strong></p>
<p>• A bit-wise identical binary of this version can be reproduced by
building from source code at commit
b07adf36019b51a10f824fdd94009985c587b1d3 associated with the tag
v_1.6.0. The release was built using Java "21" 2023-10-17 LTS
build 21.0.1.+12-LTS-29 under Linux Debian 11.6.</p>
<h2>Logback 1.5.38</h2>
<p><strong>2026-07-09 Release of logback version 1.5.38</strong></p>
<p>• In <code>HardenedObjectInputStream</code>, fixed a typo preventing
<code>Throwable</code> objects from being white-filtered. This issue was
reported in [PR <a
href="https://redirect.github.com/qos-ch/logback/issues/1045">#1045</a>](<a
href="https://redirect.github.com/qos-ch/logback/pull/1045">qos-ch/logback#1045</a>)
by <a href="https://github.com/t0rchwo0d">t0rchwo0d</a>.</p>
<p>• A bitwise identical binary of this version can be reproduced by
building from source code at commit
d04984a41fce42977466f45a2f076f0ee5cc4207 associated with the tag
v_1.5.38. Release built using Java "21" 2023-10-17 LTS build
21.0.1.+12-LTS-29 under Linux Debian 11.6.</p>
<h2>Logback 1.5.37</h2>
<p><strong>2026-06-26 Release of logback version 1.5.37</strong></p>
<ol>
<li>• Given the numerous vulnerabilities related to conditional
configuration processing based on the evaluation of Java expressions
using the Janino library, support for such expressions has been removed.
Users are offered the an <a
href="https://logback.qos.ch/translator/services/conditionalConfigMigrator.html">online
migration service</a> or the <code><condition></code> element
introduced in version 1.5.20. See the <a
href="https://logback.qos.ch/manual/configuration.html#conditional">relevant
documentation</a> for more details.</li>
</ol>
<p>• A bitwise identical binary of this version can be reproduced by
building from source code at commit
c1df7f522e648eec7b4ef6a12c8758fec0f00048 associated with the tag
v_1.5.37. Release built using Java "21" 2023-10-17 LTS build
21.0.1.+12-LTS-29 under Linux Debian 11.6.</p>
<h2>Logback 1.5.36</h2>
<p><strong>2026-06-25 Release of logback version 1.5.36</strong></p>
<p>• The 'condition' attribute in <code><if></code> elements now
reject certain references that are associated with ACE attacks. This
issue was reported by "yulate" (<a
href="mailto:yulate531@gmail.com.com">yulate531@gmail.com.com</a>) and
registered as <a
href="https://www.cve.org/cverecord?id=CVE-2026-13006">CVE-2026-13006</a>.
<strong>Please note that version 1.5.37 provides the full fix to this
vulnerability.</strong></p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/qos-ch/logback/commit/57759f433000a133088ef0441038963134437fbd"><code>57759f4</code></a>
prepare release 1.6.1</li>
<li><a
href="https://github.com/qos-ch/logback/commit/175f99f2093ae07f4c8d44f93b800f65b03a6e19"><code>175f99f</code></a>
fix imports</li>
<li><a
href="https://github.com/qos-ch/logback/commit/4b8773ed127fdc62b85a7c7ddaef10f25830788c"><code>4b8773e</code></a>
add compressionFailureLeavesOriginalFileIntact test for XZ
compression</li>
<li><a
href="https://github.com/qos-ch/logback/commit/cafaf1115fd20be19b2f7a4a09184446e8bbc04d"><code>cafaf11</code></a>
do not delete original file if compression fails</li>
<li><a
href="https://github.com/qos-ch/logback/commit/ee50125b293f5a731543de9f9f5fb72756a43464"><code>ee50125</code></a>
let the temporary file before compression be target file without the .gz
or ....</li>
<li><a
href="https://github.com/qos-ch/logback/commit/5626acc301f4039537a6c668f0f2d51989472785"><code>5626acc</code></a>
minor refactoring</li>
<li><a
href="https://github.com/qos-ch/logback/commit/d97da4fbc0de00ca901ef78d91b9fc1850ae803f"><code>d97da4f</code></a>
minor refactoring</li>
<li><a
href="https://github.com/qos-ch/logback/commit/159c045d8f045ccf8b382775d81d83919c228cca"><code>159c045</code></a>
more defensive coding in SyslogOutputStream and in
SyslogAppenderBase</li>
<li><a
href="https://github.com/qos-ch/logback/commit/9427d6b23d5a692c8a76a994c076ef68ded7835c"><code>9427d6b</code></a>
slight refactoring for clarity</li>
<li><a
href="https://github.com/qos-ch/logback/commit/79c4179c0b440a9dcf35bc9bda1ead2b2f90966c"><code>79c4179</code></a>
slight refactoring</li>
<li>Additional commits viewable in <a
href="https://github.com/qos-ch/logback/compare/v_1.5.32...v_1.6.1">compare
view</a></li>
</ul>
</details>
<br />
Updates `ch.qos.logback:logback-classic` from 1.5.32 to 1.6.1
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/qos-ch/logback/releases">ch.qos.logback:logback-classic's
releases</a>.</em></p>
<blockquote>
<h2>Logback 1.6.1</h2>
<p><strong>2026-07-28 Release of logback version 1.6.1</strong></p>
<p>• In TimeBasedRollingPolicy, when the file option is set, the
intermediate file renamed before asynchronous compression now receives
the target archive name without the compression suffix (e.g.
<code>.gz</code>, <code>.zip</code>, <code>.xz</code>). Previously it
used a nanotime-based <code>.tmp</code> suffix. This makes the file
easier to identify if compression fails during rollover. (See also the
following paragraph.)</p>
<p>• On GZ, ZIP, or XZ compression failure, the original (uncompressed)
log file is no longer deleted. Compression strategies now delete the
source file only after successful compression and emit a warning that
the original was left intact.</p>
<p>• ConsoleAppender with <!-- raw HTML omitted --> now probes JLine's
org.jline.jansi.AnsiConsole first and falls back to the legacy
FuseSource org.fusesource.jansi.AnsiConsole class. This keeps ANSI
coloring working after Jansi moved under the JLine project. The optional
org.jline:jansi-core artifact is declared as a dependency alongside the
existing FuseSource jansi dependency. A preferredJansiClassName property
was added for tests. This issue was reported in <a
href="https://redirect.github.com/qos-ch/logback/issues/1043">issues/1043</a>
by <a href="https://github.com/seonwooj0810">seonwoo_jung</a> who also
provided the relevant PR.</p>
<p>• LayoutWrappingEncoder now reports an error at start() when no
layout is set and guards encode() against a null layout. Previously, a
missing layout (for example after an ignored <!-- raw HTML omitted
-->/<!-- raw HTML omitted -->/<!-- raw HTML omitted --> branch) allowed
the encoder to start and then fail with a NullPointerException on every
event, resulting in silent log loss. This issue was reported in <a
href="https://redirect.github.com/qos-ch/logback/issues/1046">issues/1046</a>
by <a href="https://github.com/seonwooj0810">seonwoo_jung</a> who also
provided the relevant PR.</p>
<p>• FileCollisionAnalyser now detects file collisions involving nested
appenders of SiftingAppender. When the nested file or fileNamePattern
does not textually reference the discriminator key (e.g. ${userId}), a
warning is issued at configuration time naming the appender, the key,
and the shared target. This closes a gap where statically declared file
appenders were checked but sifted nested appenders were not. This
enhancement was contributed in [PR <a
href="https://redirect.github.com/qos-ch/logback/issues/1041">#1041</a>](<a
href="https://redirect.github.com/qos-ch/logback/issues/1041">qos-ch/logback#1041</a>)
by <a href="https://github.com/seonwooj0810">seonwoo_jung</a>.</p>
<p>• More defensive handling in SyslogOutputStream and
SyslogAppenderBase: the close() method now ensures that resources are
closed, writes and flushes check that the underlying resources are in a
valid state and fallback to no-op otherwise.</p>
<p>• A bit-wise identical binary of this version can be reproduced by
building from source code at commit
57759f433000a133088ef0441038963134437fbd associated with the tag
v_1.6.1. The release was built using Java "21" 2023-10-17 LTS
build 21.0.1.+12-LTS-29 under Linux Debian 11.6.</p>
<p>• See <a
href="https://logback.qos.ch/news.html#1.6.1">https://logback.qos.ch/news.html#1.6.1</a>
for the original text.</p>
<h2>Logback 1.6.0</h2>
<p><strong>2026-07-23 Release of logback version 1.6.0</strong></p>
<p>• Removed certain deprecated variables, methods, and classes. For the
list of removed members see <a
href="https://logback.qos.ch/notes/release_1.6.0.txt">release_1.6.0.txt.</a></p>
<p>• In <code>AsyncAppenderBase</code>, the
<code>put(ILoggingEvent)</code> method now has the protected modifier to
allow access from derived classes. This change was requested by Thomas
Skjølberg in <a
href="https://redirect.github.com/qos-ch/logback/pull/1053">pr#1053</a>.</p>
<p>• Bump SLF4J dependency to version 2.0.18.</p>
<p>• <strong>See also the overview of the <a
href="https://logback.qos.ch/news.html#latest_stable">1.6.x
series</a>.</strong></p>
<p>• A bit-wise identical binary of this version can be reproduced by
building from source code at commit
b07adf36019b51a10f824fdd94009985c587b1d3 associated with the tag
v_1.6.0. The release was built using Java "21" 2023-10-17 LTS
build 21.0.1.+12-LTS-29 under Linux Debian 11.6.</p>
<h2>Logback 1.5.38</h2>
<p><strong>2026-07-09 Release of logback version 1.5.38</strong></p>
<p>• In <code>HardenedObjectInputStream</code>, fixed a typo preventing
<code>Throwable</code> objects from being white-filtered. This issue was
reported in [PR <a
href="https://redirect.github.com/qos-ch/logback/issues/1045">#1045</a>](<a
href="https://redirect.github.com/qos-ch/logback/pull/1045">qos-ch/logback#1045</a>)
by <a href="https://github.com/t0rchwo0d">t0rchwo0d</a>.</p>
<p>• A bitwise identical binary of this version can be reproduced by
building from source code at commit
d04984a41fce42977466f45a2f076f0ee5cc4207 associated with the tag
v_1.5.38. Release built using Java "21" 2023-10-17 LTS build
21.0.1.+12-LTS-29 under Linux Debian 11.6.</p>
<h2>Logback 1.5.37</h2>
<p><strong>2026-06-26 Release of logback version 1.5.37</strong></p>
<ol>
<li>• Given the numerous vulnerabilities related to conditional
configuration processing based on the evaluation of Java expressions
using the Janino library, support for such expressions has been removed.
Users are offered the an <a
href="https://logback.qos.ch/translator/services/conditionalConfigMigrator.html">online
migration service</a> or the <code><condition></code> element
introduced in version 1.5.20. See the <a
href="https://logback.qos.ch/manual/configuration.html#conditional">relevant
documentation</a> for more details.</li>
</ol>
<p>• A bitwise identical binary of this version can be reproduced by
building from source code at commit
c1df7f522e648eec7b4ef6a12c8758fec0f00048 associated with the tag
v_1.5.37. Release built using Java "21" 2023-10-17 LTS build
21.0.1.+12-LTS-29 under Linux Debian 11.6.</p>
<h2>Logback 1.5.36</h2>
<p><strong>2026-06-25 Release of logback version 1.5.36</strong></p>
<p>• The 'condition' attribute in <code><if></code> elements now
reject certain references that are associated with ACE attacks. This
issue was reported by "yulate" (<a
href="mailto:yulate531@gmail.com.com">yulate531@gmail.com.com</a>) and
registered as <a
href="https://www.cve.org/cverecord?id=CVE-2026-13006">CVE-2026-13006</a>.
<strong>Please note that version 1.5.37 provides the full fix to this
vulnerability.</strong></p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/qos-ch/logback/commit/57759f433000a133088ef0441038963134437fbd"><code>57759f4</code></a>
prepare release 1.6.1</li>
<li><a
href="https://github.com/qos-ch/logback/commit/175f99f2093ae07f4c8d44f93b800f65b03a6e19"><code>175f99f</code></a>
fix imports</li>
<li><a
href="https://github.com/qos-ch/logback/commit/4b8773ed127fdc62b85a7c7ddaef10f25830788c"><code>4b8773e</code></a>
add compressionFailureLeavesOriginalFileIntact test for XZ
compression</li>
<li><a
href="https://github.com/qos-ch/logback/commit/cafaf1115fd20be19b2f7a4a09184446e8bbc04d"><code>cafaf11</code></a>
do not delete original file if compression fails</li>
<li><a
href="https://github.com/qos-ch/logback/commit/ee50125b293f5a731543de9f9f5fb72756a43464"><code>ee50125</code></a>
let the temporary file before compression be target file without the .gz
or ....</li>
<li><a
href="https://github.com/qos-ch/logback/commit/5626acc301f4039537a6c668f0f2d51989472785"><code>5626acc</code></a>
minor refactoring</li>
<li><a
href="https://github.com/qos-ch/logback/commit/d97da4fbc0de00ca901ef78d91b9fc1850ae803f"><code>d97da4f</code></a>
minor refactoring</li>
<li><a
href="https://github.com/qos-ch/logback/commit/159c045d8f045ccf8b382775d81d83919c228cca"><code>159c045</code></a>
more defensive coding in SyslogOutputStream and in
SyslogAppenderBase</li>
<li><a
href="https://github.com/qos-ch/logback/commit/9427d6b23d5a692c8a76a994c076ef68ded7835c"><code>9427d6b</code></a>
slight refactoring for clarity</li>
<li><a
href="https://github.com/qos-ch/logback/commit/79c4179c0b440a9dcf35bc9bda1ead2b2f90966c"><code>79c4179</code></a>
slight refactoring</li>
<li>Additional commits viewable in <a
href="https://github.com/qos-ch/logback/compare/v_1.5.32...v_1.6.1">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>
Same PR as #6396, just with the conflicts resolved and some fixes on
top. Original commit by @saul1310 is preserved as-is; everything else is
a follow-up commit.
Refs #6272
## Conflicts
#6396 was written before the frontend was restructured, so all four
files it touched moved (`frontend/src/**` -> `frontend/editor/src/**`)
and `LinkLayer.tsx` had drifted. Cherry-picked with rename detection and
re-resolved against current `main`.
## Fixes on top
- **Reuse the existing platform seam instead of adding a second one.**
`main` already has `@app/platform/*` seams with per-flavour
implementations; #6396 added a parallel `@app/utils/openExternalUrl`
core+desktop pair that re-implemented the Tauri shell call already in
`desktop/platform/openExternal.ts`. Split into a pure sanitiser
(`@app/utils/externalUrl`) and a platform seam
(`@app/platform/openExternalTab`), with the desktop impl delegating to
the existing `openExternal`.
- **Kept PDF links off the `openExternal` seam.** That seam is for
leave-and-return redirects (Stripe) and its saas impl is
`window.location.assign` - routing PDF links through it would navigate
the whole app away from the user's document. `openExternalTab` always
opens alongside the app; desktop shadows it to escape the webview.
- **Fixed the same defect in two sibling call sites** that #6396 didn't
cover: `BookmarkSidebar` (bookmark URI / LaunchAppOrOpenFile actions)
and `useAnnotationMenuHandlers` (annotation menu "go to link"). Both
called `window.open` on an unsanitised PDF-supplied URI, so on desktop
they trapped the link in the webview exactly like the viewer did.
- **Dropped the unguarded fallback.** The old code fell back to
`window.open(uri)` when `new URL()` threw, so an unparseable URI
bypassed the allowlist entirely. It is now blocked.
- Empty/whitespace URIs are blocked rather than silently resolving to
the app's own page via the base URL.
- Tests: sanitiser cases (casing, leading whitespace, `data:`,
`vbscript:`, unparseable), a core seam test asserting
new-tab-not-navigate, and a desktop seam regression test asserting the
URL goes to the OS rather than `window.open`.
- **`openExternalTab` now re-validates its own input.** Every caller
sanitises first, so nothing reached it unvalidated - but it is the sink
that hands a URL to `window.open` (executes `javascript:` in our origin)
or to an OS handler on desktop, and its safety shouldn't depend on
callers remembering. Both impls fail closed, with tests that call them
directly with `javascript:`/`data:`/`file:`/`ftp:`.
## Unrelated fix included (flagged deliberately)
The last commit fixes `frontend/editor/vitest.config.ts`: `testTimeout:
10000` was set on the root `test` block, but tests all run under
`projects`, which do not inherit it - so the whole suite has silently
been running at vitest's 5s default.
This is not cosmetic. It made `task check` fail intermittently on
unrelated portal specs (`demoData`, `ConnectionModal`); the ConsignO
test takes 2966ms with only the portal project running, i.e. 59% of a
budget it was never meant to have, so any CPU contention tips it over.
Proven with an identical 6.5s probe test: times out at 5000ms on the old
config, passes at 6512ms on the fixed one.
Happy to split this into its own PR if preferred - it is here because
the gate could not be trusted without it.
## Validation
Typecheck passes for all 7 build flavours (core, proprietary, saas,
desktop, cloud, prototypes, portal); ESLint, Prettier, dpdm and the full
1662-test vitest suite pass.
Driven live against the dev server + backend with a PDF carrying five
URI annotations (https, `javascript:`, mailto, `file:`, relative). 14/14
behavioural checks pass on this branch; 5 of them fail on `main`:
| check | main | this PR |
| --- | --- | --- |
| safe https link exposes real href (copy-link) | `href="#"` |
`https://example.com/safe-link?a=1` |
| link opens in new tab / tabnabbing-proof | no `target`/`rel` |
`_blank` + `noopener noreferrer` |
| mailto link exposes real href | `href="#"` | `mailto:test@example.com`
|
| relative URI resolved against app origin | `href="#"` | resolved |
| `javascript:` / `file:` never reach href | blocked | blocked |
| clicking blocked link doesn't execute or navigate | ok | ok |
| clicking safe link opens new tab at source URL | - | ok, app not
navigated away |
---
## 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)
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings
### Testing (if applicable)
- [x] I have run `task check` to verify linters, typechecks, and tests
pass
- [x] I have tested my changes locally
---------
Co-authored-by: Saul <saulifshin.cs@gmail.com>
Review Flow PR 2 of 5. Editor tool failures now reach the same durable
queue as failures from folders, buckets and webhooks.
## What's added
**A report endpoint** — `POST /api/v1/file-run-events/reports`, open to
any authenticated user. Takes four fields: `operation`, `errorCode`,
`fileIds`, `detail`. No team, no actor, no filename: the first two come
from the session, the third is never a field. Refused with 400 above 200
file ids, and nothing is written when refused.
**Automatic reporting from every tool** — wired into `useToolOperation`,
so no per-tool work is needed. Client-side refusals (an unsupported
format that never reaches the server) are reported too. User
cancellations are not.
**Error codes parsed from Blob bodies as well as JSON** — a
download-typed tool call fails with a Blob, so `errorCodeOf` handles
both shapes.
**Source attribution for unattended runs** — `sourceId` is threaded from
`PolicyRunner` through `PolicyRun` to the recorded row and out to the
wire, so a folder, bucket or webhook failure names what fed it.
Previously it had none.
**Deleting a file closes its failures** — `FileContext.removeFiles`
notifies `POST /removed-files`, which transitions those incidents to
`FILE_REMOVED`. Terminal, so they leave every reviewer's queue. The rows
stay for audit.
**The queue can be emptied** — reads now default to open statuses only;
ask for a status explicitly to see closed rows.
## Behaviour changes
- **Editor failures dedup per person.** `RecordFailure.scopeRef()`
includes the actor for TOOL-origin rows, so two people hitting the same
failure on the same file are two incidents rather than one. Processor
rows are unaffected and their dedup key is byte-identical to before.
- **`UNKNOWN` offers only Dismiss.** Acknowledge is no longer offered on
it.
- **Background reports no longer raise a toast.** Both calls pass
`suppressErrorToast`, so a failed report is silent as intended;
previously a core build showed the user a "Not Found" toast on every
tool failure.
## What is stored
File ids only, never names. The request type has no filename field, and
a `fileNames` value handed to the client reporter is accepted and
ignored.
One caveat to review deliberately: the free-text `detail` is stored
**verbatim**. `RecordFailure` truncates it at 2000 characters and
nothing else; the redaction that used to strip name-shaped text was
reverted in `024899f3f6` because it made an unclassified failure
impossible to act on. A backend message that embeds a filename
(LibreOffice conversion errors, IO errors) will therefore persist that
text and show it to a team leader.
## How to test
Needs a proprietary or SaaS build with login enabled. `task dev:all`
gives you one.
1. **Report a failure from a tool.** Open a PDF, run **Remove Password**
on it with a wrong password. Nothing visible changes for you: reporting
is silent by design.
2. **See it recorded.** Go to `/processor/documents` and scroll to
**Failures** (dev builds only). A row appears titled "Password-protected
document", with `Hit by <your user>`. Press **Show raw JSON** to see
exactly what was stored.
3. **Confirm no filename is stored as data.** In that JSON, `fileId` is
an opaque uuid and there is no name field. Note the `detail` string may
contain a filename if the backend put one in its message, per the caveat
above.
4. **Confirm the request is capped.** In DevTools, POST to
`/api/v1/file-run-events/reports` with 201 entries in `fileIds`. It
returns 400 naming the limit, and no rows are added.
5. **Deleting a file clears its failure.** Back in the editor, delete
the file you just failed on. Refresh the failures list: its row is gone
from the default view. Filter by `FILE_REMOVED` to see it still exists.
6. **Two people, two incidents.** Have a colleague fail the same tool on
their own copy of the same file. Two rows, not one occurrence count.
## Migration
`source_id` is a new column and `FILE_REMOVED` a new status value. Both
are already in the SaaS migration ([Stirling-PDF-SaaS
#322](https://github.com/Stirling-Tools/Stirling-PDF-SaaS/pull/322));
self-hosted picks them up from `ddl-auto`.
# Description of Changes
This fixes the a11y violations that are currently failing in the
nightlies in dark mode. Now that we're down to 0 baseline, we can
require the a11y tests to pass in PRs before they merge, so I've changed
that, and I've also made it so that the nightly will report failures in
both light and dark mode instead of just light mode if that fails.
<img width="2560" height="838" alt="image"
src="https://github.com/user-attachments/assets/b0322182-f6ff-4dea-9a1c-5a6c9c9c5439"
/>
Five unrelated snags in the processor (portal) UI, plus fixes they
turned up. No backend changes.
`84 files changed, +892 / −3364`
## Fat CTA buttons
- New `fat` prop on the SUI `Button`: 2.75rem tall, 1.25rem side
padding, 0.75rem corners, semibold. Composes with all four
variants/accents.
- Applied to the page-header CTA on Sources, Documents, Pipelines, Users
(both), Usage, Integrations, Infrastructure — 8 buttons, all in line
with a page title. Nothing else.
- `LandingActions` migrated onto the prop; `.landing-btn-primary` /
`.landing-btn-secondary` and their four `!important`s deleted. The
editor landing CTAs come down 4px with everything else.
- Infrastructure's header CTA is now primary; its "Create key" dropped
to secondary so they stop competing.
<!--IMG:buttons-->
## Documents empty state
- "Connect a source" opened the Sources *page*; it now opens the
`SourceModal` connect flow in place, no route change.
- No extra cache wiring: `SourceModal` already invalidates the sources
query.
<!--IMG:documents-->
## Infrastructure tabs
- Only API Keys and Audit Logs hit real endpoints. Deployments,
Security, Models and Storage read mock-only `/v1/infrastructure/*` that
no backend serves.
- Those four are now disabled: native `disabled`, out of the keyboard
tab order, `aria-disabled`, with the view refusing non-enabled keys as a
second guard.
- Real tabs moved leftmost; API Keys is the default; `?tab=` deep links
validated against the enabled set (the home flow's audit link still
works).
- Deleted: 4 tab components, their fetch fns and ~25 dead types, MSW
handlers, fixtures (908 → 253 lines), dead CSS, unused formatters, 240
lines of `en-US` strings. Most of the −3364.
- Page subtitle no longer advertises the disabled tabs.
<!--IMG:infrastructure-->
## Surface consolidation
- New `Surface` primitive (`sui-surface`): fill, hairline, radius, no
shadow. Kept separate from `sui-nav-surface` so nav chrome can diverge
later.
- `Card` composes it and no longer draws its own shadow — this changes
editor Card usages too, by design.
- SUI primitives that are surfaces adopt it: `MetricCard`,
`MetricStrip`, `NodeCard`, `Table`, `Collapsible`, `CodeBlock`.
- The portal gets its own `.portal-surface` with the same three
declarations, applied to 19 elements. A `sui-` class belongs to the
component that emits it, so feature markup doesn't wear one.
- `raised` variant = one subtle shadow for a surface in front of another
surface (the flow diagram's tiles). Same fill as its parent, so nesting
never shifts a region's colour. Dark has its own value.
- Floating chrome (modals, drawers, dropdowns, assistant, sidebar) keeps
its elevation; sunken wells stay sunken.
<!--IMG:surfaces-->
## Sources list
- Centred "No sources connected yet" empty state removed — it duplicated
the header CTA and pushed the table down the page. The header's "Connect
source" is the single way in.
## Drive-by fixes
- The connect flow rendered unstyled outside the Sources view:
`.portal-conn-picker__*` / `.portal-sources__connection-*` lived in
`views/Sources.css`, which none of the five components rendering them
imported. Moved to `components/sources/connections.css`.
- Three inert custom properties (`--surface-input`, `--color-border-2`,
`--text-default`) are defined nowhere in the codebase —
`.portal-conn-picker__card` had no fill at all as a result.
- Dead CSS removed from `Sources.css` (grep-verified unused): old
expanded-row panel + its keyframes, type-card block.
## Testing
- `task frontend:check` — typecheck, lint (oxlint + 4 theme-lint passes
+ stylelint), format, 238 files / 2063 tests.
- `frontend:typecheck:all` across all 9 tsconfigs.
- `frontend:storybook:a11y:changed` — 119 stories, light and dark, zero
violations, no regressions vs baseline.
- New tests: `Infrastructure.test.tsx` (tab order, default, disabled
behaviour, deep-link filtering) and a Documents test that the
empty-state CTA opens the modal without navigating.
- Merged `origin/main` (#7438 replaced `PipelineHeader` with the new
Create/Edit headers); full suite green at 240 files / 2072 tests after
the merge.
# Description of Changes
## Harden GitHub Actions secret handling
Moves secrets behind deployment environments, removes the GitHub App
token from
workflows that only comment and label, and moves PR preview images to
GHCR so the
preview path needs no registry credential.
Builds on #6005 by @dagecko — that commit is preserved with original
authorship,
rebased onto current main.
### Extract secrets from `run:` blocks (@dagecko, #6005 rebased)
- Secrets referenced in shell bodies moved to step-level `env:` so
values never
reach a rendered command line
- Two `workflow_dispatch` inputs moved out of shell interpolation
(`multiOSReleases`, `push-docker-base`)
- Dropped the hunks main has since solved — `setup-uv`, `reviewdog`,
`build-push-action` and `github-script` are all pinned newer on main now
- Fixed a bug in the original: `PR-Demo-cleanup.yml` uses a **quoted**
`<< 'ENDSSH'`
heredoc, so rewriting `${{ secrets.DOCKER_HUB_USERNAME }}` to
`${DOCKER_HUB_USERNAME}`
would have sent the literal string to the VPS and expanded to empty,
silently
orphaning preview images behind `|| true`
### Gate secret-bearing jobs behind environments
- `environment:` added to 15 jobs across 10 workflows, mapping to
`release-signing`,
`docker-publish`, `package-publish`, `pr-preview` and `bot-identity`
- Environment branch/tag policies are enforced by GitHub before the job
starts, so
editing the workflow file cannot bypass them
- Four jobs deliberately **not** gated — `tauri-build`,
`frontend-backend-licenses-update`,
`swagger` and `push-docker-base` would fail their own triggers under the
current
policies and need restructuring first
- Removed the `testMain` trigger from `push-docker` — the branch doesn't
exist and
isn't in the environment's policy
### Publish PR previews to GHCR instead of Docker Hub
- Preview images now go to `ghcr.io/stirling-tools/stirling-pdf-test`,
authenticated
with `GITHUB_TOKEN` rather than `DOCKER_HUB_API`
- Docker Hub personal access tokens cannot be scoped to a single
repository, so the
preview path was holding the same credential that publishes `s-pdf` and
`stirling-pdf`
- `DOCKER_HUB_API` no longer appears in any PR-reachable workflow
- Login now precedes every `docker manifest inspect` —
`deploy-on-v2-commit` had them
reversed, which only worked because the Docker Hub repo was public
### Use `GITHUB_TOKEN` for comment and label workflows
- Seven workflows no longer mint a GitHub App token; only
`sync_files_v2`,
`sync-portal-docs` and `frontend-backend-licenses-update` still do, so
unattended
auto-merge is unaffected
- `permissions:` blocks derived per job from the API calls each actually
makes —
these were previously inert, since an App installation token ignores
them, and one
job had no block at all
- Comment-threading matchers updated to `github-actions[bot]` so
workflows still edit
their own previous comment instead of posting duplicates
- Removed the App token from the `refs/pull/N/merge` checkout in
`PR-Demo-Comment-with-react` and set `persist-credentials: false` — it
was written
into `.git/config` of an untrusted tree that the same job then builds
- Fixed a script injection in `check_toml.yml`: a fork-controlled branch
name was
interpolated into `actions/github-script` JS source, with validation
running after
the injected code had already executed. Values now come from
`process.env` and are
validated before use.
---
## 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.
---------
Co-authored-by: dagecko <cnyhuis@vigilantnow.com>
# Description of Changes
This PR refactors our JPA entity classes to replace Lombok's `@Data` and
auto-generated `@EqualsAndHashCode` annotations with explicit Lombok
annotations and custom, JPA-compliant `equals()` and `hashCode()`
implementations.
### Rationale
Lombok's default `@Data` and `@EqualsAndHashCode` annotations are not
recommended for JPA entities. They often lead to:
- Severe performance issues (e.g., loading lazy collections when
evaluating `hashCode` or `toString`).
- Identity mismatches or collection bugs (e.g., when database-generated
IDs transition from `null` to assigned, breaking the entity's lookup in
a `Set` or `Map`).
This change ensures all JPA entities use safe Hibernate proxy checking
and use only the entity's database identifier for equality and hash code
calculations.
<!--
Please provide a summary of the changes, including:
- What was changed
- Why the change was made
- Any challenges encountered
Closes #(issue_number)
-->
---
## 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.
---------
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
# Description of Changes
Follow-up to #7314, which fixed the IndexedDB blob rejection itself.
This one fixes the remaining WebKit engine gaps, fixes the ways that
class of failure surfaced to the user, and adds the cross-browser signal
that would have caught them on the PR instead of six weeks later.
## Why this exists
Two total WebKit outages sat on `main` for weeks:
1. pdf.js reads its text stream with `for await (… of readableStream)`,
and WebKit has no `ReadableStream[Symbol.asyncIterator]`. **All** pdf.js
text extraction threw `TypeError: undefined is not a function` —
Compare, read-aloud and the PDF text editor were dead on Safari.
2. IndexedDB in WebKit rejects Blob/File values with `UnknownError:
Error preparing Blob/File data to be stored in object store`, so nothing
persisted and every reload came back empty.
Neither was caught, because the existing specs never did the work. The
Compare specs filled both slots and asserted the button was enabled;
none of them clicked it. The persistence specs asserted a *filename*
reappeared after a reload, which only needs the metadata record, not the
bytes.
Every failure here **looked like success** — empty panes, blank
thumbnails, a `src` that was set but empty. That shapes the tests more
than the fixes.
## WebKit engine gaps
- **`ReadableStream[Symbol.asyncIterator]`**, installed at the entry
point before any PDF work starts. The lock discipline is the subtle
part: releasing is idempotent, is *not* done after a successful read,
and *is* done in the read's error steps — `for await` never calls
`return()` when `next()` rejects, so nothing else would ever unlock an
errored stream.
- **`requestIdleCallback`**, installed once instead of guarded at each
call site. This one wasn't broken, it was mistimed: the local fallbacks
fired at 200ms and 1000ms, landing the pdfium WASM compile on top of the
app's first renders. The shim honours the caller's full timeout, so
`{timeout: 2000}` means 2000ms.
- **`convertToBlob()` does not fail on a format it can't encode.** Per
spec it silently serialises to PNG, so asking for WebP and getting PNG
back looks like success. Canvas output now probes what the engine really
produced (once per realm) and uses the best lossy format it honours. PNG
of a rendered page is several times the size of the equivalent WebP or
JPEG, held as object URLs for every page on screen, on the engine with
the tightest renderer memory budget.
## WebKit storage failures
These read as generic transaction hygiene. They aren't — a refused blob
write **aborts its transaction**, which is the mechanism that turned a
WebKit rejection into a hang.
- **Blob refusal is remembered from any write**, not just the initial
`add`. WebKit reports it when it can't write the blob's *backing file*,
which is per-operation — an engine that accepted the add can still
refuse the rewrite, and every read-modify-write rewrites the record with
its body attached.
- **Aborted transactions no longer hang.** Read-modify-write moves to a
single `updateRecord` helper that owns its transaction, guards it once,
and resolves on **commit** rather than on the put's `onsuccess`. The
previous shape — two promises over one shared transaction, with an
`await` between the get and the put — put the abort guard on the read,
leaving the write with no handler at all. `persistVersionedOutputs`
awaits that, and `.catch` can't rescue a promise that never settles, so
tool outputs could silently stop persisting.
- **Stored blobs are no longer re-wrapped on read.** Since #7175 the
record holds the `File` itself; wrapping it in `new Blob([record.data])`
can cost WebKit the backing handle, giving you an object that looks
valid and reads as empty.
- **The file sidebar reaches a resting state** when the library can't be
read, instead of spinning forever on a rejection nobody observes. It
carries on with the in-memory workbench files: an unreadable library
should cost the user their history, not the file they're working on.
- **Thumbnail failures are logged.** Three `catch {}` blocks returned
`""`, and an empty thumbnail is indistinguishable from "this file has no
preview" — which is how outage #1 hid as a cosmetic nicety.
## CI
`main` now runs the whole stubbed suite once per engine (#7304), so the
new `@engine-capability` specs get chromium, firefox and webkit for
free. They assert the primitives actually work — a **counted**
comparison, a raster thumbnail data URL with real payload, and a page
rendered from a file restored by a reload — rather than that the UI
rendered. Deliberately small: anything added there is paid for three
times per PR, so add depth, not breadth. Run them alone with `task
e2e:cross-browser -- --grep @engine-capability`.
The cross-browser projects now share the stubbed project's viewport. At
the device presets' default 1280x720 a layout difference would fail
these specs on Firefox/WebKit only, which reads as an engine outage.
`vite.config.ts` gains a `worker.plugins` entry so `@app/*` resolves
inside worker bundles. Worker bundles are a separate Rollup pass and
don't inherit `plugins`, so the alias worked in the app and failed in a
worker — previously worked around with a relative import plus a lint
exemption, which silently bypasses the layer cascade.
## Verification
- `task frontend:check` green: typecheck, oxlint, theme lint, stylelint,
prettier, 215 test files / 1841 tests.
- The `@engine-capability` suite passes on Chromium and WebKit locally.
- **Negative control:** with the `ReadableStream` shim removed, the
WebKit comparison spec fails at the Deletions/Additions assertion — the
exact reported Safari symptom. Restored, and it passes. Both the fix and
the test that guards it are load-bearing.
- The worker alias change verified both ways: the build inlines the
encoding probe into the worker chunk, and removing `worker.plugins`
fails with `Rollup failed to resolve import
"@app/utils/canvasImageEncoding"`.
- The abort regression test aborts the transaction mid-write and asserts
`markFileAsProcessed` settles. Before the fix it never settles and the
test times out.
## Split out of this PR
Two things in earlier revisions of this branch were engine-agnostic —
found via the same symptom, not the same cause — and now have their own
PRs:
- **#7416** — blocked IndexedDB upgrades hanging the file library
(multi-tab lifecycle, the concurrent-open race, `onversionchange`).
- **#7417** — the thumbnail TTL rewriting the whole library on every
listing.
`FileSidebar`'s try/catch appears in both this PR and #7416,
identically: a WebKit rejection and a blocked-open rejection both have
to stop stranding the spinner. Whichever merges second is a no-op for
that file.
## Known gaps
- The blob-refused **rewrite** recovery in `updateRecord` isn't
unit-tested. `fake-indexeddb` never returns Blob values from a read, so
the branch that converts to a copy can't be reached there. Noted in the
test file.
- For the same reason, `fileFromRecord`'s "hand the stored File back
untouched" path is only covered on a real engine, by the reload spec.
- Nothing asserts that `src/index.tsx` imports the shims. The unit suite
installs the same module via `setupTests.ts` (jsdom has the same gaps
WebKit does), so a future regression where the entry point drops the
import would still be green under vitest.
- `FileSidebar`'s resting-state fix loses its E2E coverage until #7416
lands — forcing WebKit's blob refusal from a spec isn't practical, which
is why that spec blocks the database instead.
---
## 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)
- [x] My changes generate no new warnings
### Documentation
- [x] 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)
## Description of Changes
- Add `/app/saas` and `/buildSrc` to Dependabot's Gradle update
directories.
- Include `buildSrc/**` in every Gradle `actions/cache` key.
- Ensure changes to shared Gradle build logic invalidate the dependency
cache.
This keeps dependency updates and CI caching aligned with the
repository's current Gradle build structure.
---
## 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 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)
### Testing
- [ ] I have run `task check` to verify linters, typechecks, and tests
pass
- [ ] I have tested my changes locally.
One search bar for the whole app, in both the Editor and the Processor.
**Cmd/Ctrl+K** from anywhere.
- **Editor** — your files, tools, settings, and everything in the
Processor: its pages plus live users, policies, pipelines, sources, and
full-text developer docs.
- **Processor** — the same bar over its own data (users, policies,
pipelines, sources, docs) plus tools and settings. Files stay
editor-only; pages stay out (the sidebar covers them).
- **Filter chips** (and typed prefixes like `policy: invoice`) narrow to
one lane; each group shows a few results with **Show more** to expand.
- **Access-gated**: users only see lanes and settings sections they can
actually open — no Processor chips, results, or entity fetches without
portal access, no admin settings for non-admins, no account-bound
sections for anonymous SaaS sessions. Asserted by unit tests and a
stubbed Playwright suite.
- Selecting a result takes you straight there, across apps when needed —
results group under collapsible sections.
Replaces the separate search boxes that previously lived in the file
sidebar, the tool panel header, the settings modal, and the docs page.
### Editor
<img
src="https://gist.githubusercontent.com/reecebrowne/5879e797c5ab6abc027a7bfd5cd4de17/raw/editor-search.png?v=2"
width="800" alt="Editor super search" />
### Processor
<img
src="https://gist.githubusercontent.com/reecebrowne/5879e797c5ab6abc027a7bfd5cd4de17/raw/portal-search.png?v=2"
width="800" alt="Processor super search" />
---------
Co-authored-by: EthanHealy01 <ethan.healy.21@gmail.com>
# Description of Changes
Refactors automatic text redaction to use JPDFium-based redaction/text
removal instead of PDFBox
Changes:
* The `RedactController` now uses the JPDFium native redaction engine
(`PdfRedactor.redact`) as the primary method for PDF redaction, with
automatic fallback to the manual redaction service if JPDFium fails or
throws an exception. This improves reliability and leverages more robust
native features when available.
* Regex patterns provided by the user are now validated before redaction
begins, ensuring invalid patterns are rejected early with clear error
messages.
* The code now trims and filters out empty or excessively long redaction
terms, preventing unnecessary processing and potential errors.
<!--
Please provide a summary of the changes, including:
- What was changed
- Why the change was made
- Any challenges encountered
Closes #(issue_number)
-->
---
## 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.
Bumps the gradle group with 1 update in the /docker/backend directory:
gradle.
Bumps the gradle group with 1 update in the /docker/embedded directory:
gradle.
Updates `gradle` from `934a520` to `e8aeffb`
Updates `gradle` from `934a520` to `e8aeffb`
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>
Bumps the ubuntu group with 1 update in the /docker/base directory:
ubuntu.
Bumps the ubuntu group with 1 update in the /docker/unoserver directory:
ubuntu.
Updates `ubuntu` from `4fbb8e6` to `561618e`
Updates `ubuntu` from `4fbb8e6` to `561618e`
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
## Summary
- add sample/example file prompts to the bug report and feature request
issue forms
- remind reporters to remove sensitive information before attaching
files
Closes#4480
## Checks
- `git diff --check`
- parsed both updated issue template YAML files with PyYAML and verified
the `sample-files` field is present
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
…:runtime
The Windows branch of jlink:runtime clears the read-only attribute jlink
leaves on the bundled JRE, so Tauri can overwrite the staged copies. It
never worked.
Task does not hand the command to cmd.exe; it runs it through its own
POSIX shell, which expands `$_` and `$false` as shell variables. Neither
is set, so both became empty and PowerShell was asked to run
ForEach-Object { .IsReadOnly = }
which errors on every file. Verified against a directory of read-only
files: the double-quoted form leaves 3 of 3 still read-only and exits
non-zero, the single-quoted form clears all 3 and exits 0.
Single quotes stop the expansion. Also add -File: without it
Get-ChildItem yields directories too, and DirectoryInfo has no
IsReadOnly property, so those iterations would fail even once the
variables survive.
The POSIX branch above is unaffected - chmod needs no variables.
# Description of Changes
<!--
Please provide a summary of the changes, including:
- What was changed
- Why the change was made
- Any challenges encountered
Closes #(issue_number)
-->
---
## 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.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Description of Changes
Remove the hard-coded five-minute client-side timeout from Automate
operations.
- Remove `AUTOMATION_CONSTANTS.OPERATION_TIMEOUT`.
- Let normal Automate requests use the API client's default no-timeout
behavior, matching regular tool execution.
- Preserve an explicitly supplied `AutomationProcessingOptions.timeout`
without applying a finite default.
- Add regression coverage verifying that Automate does not send a
client-side timeout by default.
Large PDF operations such as compression can legitimately take more than
five minutes. Previously, Axios aborted the frontend request after
300,000 ms even when the server and reverse proxy allowed the operation
to continue. The backend could continue processing while the frontend
discarded the result.
No significant implementation challenges were encountered.
Closes#7081
## Testing
The following frontend checks passed:
- Proprietary frontend TypeScript typecheck
- ESLint with zero warnings
- Circular dependency check
- Theme colour lint
- Prettier formatting check
- Focused `automationExecutor.test.ts` regression test
- Complete Vitest suite: 165 test files and 1,354 tests passed
---
## 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)
(not 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/)
(not applicable; no user-facing configuration 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)
(not applicable)
### Translations (if applicable)
- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)
(not applicable)
### UI Changes (if applicable)
- [ ] Screenshots or videos demonstrating the UI changes are attached
(not applicable; no visual changes)
### 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.
Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
# Description of Changes
This PR introduces a broad modernization of the codebase by adopting
newer Java language features and improving code readability and
maintainability.
## What was changed
- Replaced traditional `switch` statements with modern switch
expressions (`case ->`) across multiple classes.
- Replaced usages of `List#get(0)` and `List#get(size - 1)` with
`getFirst()` and `getLast()` respectively.
- Simplified conditional logic using pattern matching (e.g.,
`instanceof` and switch pattern matching).
- Refactored various utility and controller classes to reduce
boilerplate and improve clarity.
- Removed unused or redundant code (e.g., `parseClientFileIds` method in
`MergeController`).
- Improved type safety (e.g., using `Class::isInstance` instead of
`instanceof` checks in streams).
- Cleaned up Spring annotations by removing unnecessary `@Autowired`
where constructor injection is already used.
- Added a new test (`UIDataControllerTest`) to ensure correct handling
of identical JSON configs with different filenames.
- Minor formatting and style fixes (e.g., Spotless formatting
adjustment).
## Why the change was made
- To align the codebase with modern Java standards (Java 17+ features).
- To improve readability and maintainability by reducing verbosity.
- To eliminate common indexing patterns that are more error-prone.
- To standardize coding style across the project.
- To improve test coverage for edge cases discovered during refactoring.
---
## 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.
---------
Signed-off-by: Ludy87 <Ludy87@users.noreply.github.com>
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.12 to
3.4.13.
<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.13</h2>
<ul>
<li>Fixed an issue with hook removal during <code>IN_PLACE</code>
sanitization, thanks <a
href="https://github.com/koyokr"><code>@koyokr</code></a></li>
<li>Fixed an issue with hooks potentially bypassing the clone guard,
thanks <a
href="https://github.com/AkshayjainG"><code>@AkshayjainG</code></a></li>
<li>Fixed an issue with DOM clobbering via <code>ownerDocument</code>
during <code>IN_PLACE</code>, thanks <a
href="https://github.com/AkshayjainG"><code>@AkshayjainG</code></a></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/3067f774676975de12306effd6db6ad7a9a8c17f"><code>3067f77</code></a>
release: 3.4.13 (<a
href="https://redirect.github.com/cure53/DOMPurify/issues/1562">#1562</a>)</li>
<li>See full diff in <a
href="https://github.com/cure53/DOMPurify/compare/3.4.12...3.4.13">compare
view</a></li>
</ul>
</details>
<br />
[](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>
### Motivation
- `./gradlew clean` on the default/proprietary flavor did not remove
`app/saas/build` left behind by earlier SaaS builds, causing stale
artifacts to persist across flavors.
### Description
- Add a `clean` hook in `build.gradle` that deletes `app/saas/build`
(`tasks.named('clean') { delete
layout.projectDirectory.dir('app/saas/build') }`) so the root `clean`
always removes SaaS artifacts even when `:saas` is not included.
### Testing
- Created `app/saas/build/clean-regression-marker`, ran `./gradlew
clean`, and verified the `app/saas/build` directory was deleted
(success).
- Ran `./gradlew spotlessCheck test`; Spotless checks passed but the
full test run reported environment-dependent unit test failures
unrelated to this change (8 failures), and `task backend:check` could
not be executed because the `task` CLI is not available in the
environment.
------
[Codex
Task](https://chatgpt.com/codex/cloud/tasks/task_e_6a7b0a9aac888325aa78e05c953355dc)
Auto-generated by stirlingbot[bot]
This PR updates the backend license report based on dependency changes.
Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
### Motivation
- Group Storybook packages so Dependabot updates `storybook` and all
`@storybook/*` packages together and reduce churn from many separate
updates.
### Description
- Add a `storybook` group to the `npm` groups in
`.github/dependabot.yml` with patterns `storybook` and `@storybook/*`.
- No other configuration changes were made.
### Testing
- Validated the new group patterns with `ruby -e 'require "yaml"; ...'`,
which confirmed the `storybook` group and patterns and returned success.
- Ran `git diff --check` to ensure there are no whitespace or patch
errors, which succeeded.
- Inspected the updated file with `nl -ba .github/dependabot.yml | sed
-n '100,120p'` to confirm the inserted lines are present.
------
[Codex
Task](https://chatgpt.com/codex/cloud/tasks/task_e_6a7afe74fe7083259900e8d9018f595c)
## Description
Moves the repository-wide VS Code settings out of PR #7386 into a
dedicated pull request.
## Changes
- Configure Ruff for Python files and point it at the engine project
configuration.
- Add repository-root paths for stylelint and CSS/SCSS/Less at-rule
handling.
- Enable Oxc safe fixes on save for JavaScript and TypeScript files.
- Add Even Better TOML formatter settings for the repository's TOML
files.
This keeps PR #7386 focused on the Python tooling migration while
preserving the editor configuration as a separately reviewable change.
## Validation
- Exact file diff matches the settings change from PR #7386.
- `task pre-commit:whitespace` was attempted; it is currently blocked by
pre-existing whitespace issues in
`engine/src/stirling/models/tool_io.py` and
`engine/src/stirling/models/tool_models.py`.
# Description of Changes
- `/` is now a router, not a page: signed-in users go to the processor
or the editor by role. The editor lives at `/editor`.
- `/editor` never routes — always the editor, so processor users have a
URL that won't bounce them.
- Core and desktop keep the editor at `/` (no processor, nothing to
route between).
- `/editor` signed out → `/login` → back to `/editor` after signing in.
- Signed-out visitors aren't redirected: `/` renders the app and Landing
owns it (login page / SaaS inline sign-in / backend-down screen).
- `RootGate` wraps the app instead of being its own route, so nothing
boots on the way to the processor and nothing remounts on the way to the
editor.
- Login resolves its own destination instead of bouncing through `/`.
- Replaces the old once-per-login `LoginLandingRedirect` +
sessionStorage flag. Landing flag and Settings preference unchanged.
- Separate commit: theme-lint crashed on files deleted in the working
tree (`git ls-files` is the index view). Any branch deleting a source
file hit it.
- Sign-out untouched. Tool routes stay top-level, so no deep links or
SEO break.
Future PR to allow users to configure their own routing from / for their
profile
---
## 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.
## What
Empties the light Storybook accessibility baseline — **1,058
grandfathered violations across 846 stories → 0** — so a new violation
fails the gate instead of being silently absorbed. Also burns the dark
baseline **812 → 56**; every entry left is one `main` already
grandfathers.
## The defect, repeated everywhere
A colour picked as a **fill**, chosen to carry a white label at 3:1,
reused as **text**, where the floor is 4.5:1. It recurred through status
accents, filled buttons, form labels, Mantine's light and outline
variants, CSS declarations, inline styles and the generated accent ramp.
Three systemic causes account for most of it:
- **Mantine's semantic slots were never bound.** `-text`, `-outline`,
`-light-color`, `-filled` and `-dimmed` all default to the hue's solid
fill. Both resolvers now pin them to the accessible ink for the active
scheme.
- **The tint ladder was compressed.** `--color-<hue>-50/100/200` pointed
at saturated 400-level primitives, so every "tint" background rendered
as a fill.
- **Text was faded with `opacity`**, pushing already-muted copy below
the floor. Each site now recedes via ink or surface, which is what
conveyed the state anyway.
## Dark mode
The colour resolver's dark half was empty, so dark fell through to
Mantine's stock palette — and fixing the naming violations unmasked the
contrast sitting underneath them. Both schemes now share one slot map,
since most slots are written in tokens that already flip.
The dark-only fixes: `--c-text-subtle` (3.0:1, used in 478 places), the
error and section-label inks, and the accent ramp's text step — which
light reaches by mixing toward black and dark has to reach by mixing
toward white.
## Also
- New `--c-*-solid` tokens for fills that must carry a white label,
distinct from the `--c-<tone>` values used for surfaces, borders and
icons.
- A `data-user-content-preview` opt-out for nodes rendering a facsimile
of the user's own document — WCAG governs the interface, not content
authored through it.
## Verification
- `task frontend:check:all` — green.
- Changed-set gate, both schemes, after the final rebase: **366 stories,
0 regressions**.
- Full sweep at the prior base — light **1,447 stories / 0 violations**,
dark **1,448 / 0 regressions**. The dark re-record was confirmed
key-by-key to be a strict subset of `main`'s, so nothing new is
grandfathered.
Roughly 28% of what this clears is naming and structure (`button-name`,
`label`, `aria-*`) and has no visual signature; the rest is contrast.
# Description of Changes
Scan a QR code in the Sign tool, draw your signature on your phone, and
it appears on your desktop ready to place. Rides the mobile scanner's
existing transfer sessions — no new backend endpoints.
**Desktop:** a **Mobile upload** button above the signature source
selector shows a QR code. When the signature arrives, the modal closes,
it lands in the matching source, and **placement activates
automatically** — click the PDF to place.
**Phone:** a new public `/mobile-sign` page with three tabs (same order
as the desktop sources):
- **Draw** → canvas signature. Touch-first pad (pointer events,
DPR-aware, smoothed strokes, undo/clear, black/blue ink, 3 pen sizes),
exported as a transparent PNG cropped to the ink. Compact layout in
phone landscape.
- **Photo** → image signature. "Take a photo" opens the camera directly;
"From gallery" opens the picker. A preview of the current image
signature now shows in the desktop's Image source (previously arrival
was invisible until placement — also fixes this for saved image
signatures).
- **Type** → text signature. Travels as data (text + font + colour), so
it stays *editable* on the desktop. Fonts are the sign tool's own
text-mode list.
**Security:** the transfer endpoints are unauthenticated by design
(10-min sessions, files deleted after download — same model as the
scanner). The desktop treats every arrival as untrusted: images only,
and the text payload is clamped field by field.
**Config:** new `system.enableMobileSignature` flag (default on),
independent of `enableMobileScanner`; the shared endpoints accept
either. The Tauri desktop app serves a self-contained `mobile-sign.html`
(draw-only), mirroring `mobile-upload.html`.
**Refactor:** the session lifecycle (create/poll/download/expiry) moved
out of `MobileUploadModal` into a shared `useMobileTransferSession`
hook; the scanner modal now uses it, behaviour unchanged.
Also fixes two bugs hit along the way: the signature pad collapsing to
its 150px intrinsic height (indefinite parent height), and a setState
loop in `SignSettings` when text parameters are set programmatically
(draft-sync effects ping-ponging).
## Screenshots
| Desktop: QR entry | Phone: draw | Desktop: received |
|---|---|---|
| 
| 
| 
|
## How to test
1. Open the app on an address your phone can reach (not `localhost`),
Sign tool → **Mobile upload**, scan the QR.
2. Draw → **Send to computer** → it becomes the active canvas signature
and placement is live: click the PDF to place.
3. Photo tab → arrives in the Image source with a preview. Type tab →
arrives editable in the Text source.
4. Flags: `enableMobileSignature: false` hides the button; signature
still works with the scanner disabled.
Verified end-to-end (all three kinds, portrait/landscape/tablet) plus
`task frontend:check` and the touched backend tests.
---
## 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)
### UI Changes (if applicable)
- [x] 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.
Removes
`app/saas/src/main/resources/db/migration/saas/V33__api_keys.sql`, the
only file left in that tree.
## Flyway does not run
There is no Flyway dependency in any `build.gradle` and no Flyway
configuration in any properties file. Nothing has executed these for
some time, so `V33` was never applied by anything.
SaaS schema has exactly two writers today:
1. Supabase CLI migrations in the `Stirling-PDF-SaaS` repo, applied by
that repo's PR CI
2. Hibernate `ddl-auto=update` in the app, which only ever adds
## Why delete rather than leave it
A directory of plausible-looking migrations is a trap. The next person
to make a schema change adds a `V34`, assumes it will run, and it
silently does not. I nearly did exactly that while working on
[#7414](https://github.com/Stirling-Tools/Stirling-PDF/pull/7414) before
checking whether Flyway was actually wired.
## Nothing is lost
Both tables it declared, `api_keys` and `api_key_daily_usage`, have JPA
entities (`ApiKey`, `ApiKeyDailyUsage`), so `ddl-auto` creates them.
That is already how they exist on every deployment that has them, since
Flyway was not the thing creating them.
## Checks
No references anywhere: nothing in code, config, gradle or docs mentions
`db/migration`, `flyway` or `V33`. Two code comments mention Flyway
historically, explaining why a column looks the way it does; those are
accurate history and are left alone.
`:saas:compileJava` and `:saas:processResources` green after the
removal.
# Description of Changes
Frontend CI is currently set to run `task frontend:check:all`, which
runs the tests, and then manually runs them a second time with coverage
enabled. This adds about 4 minutes onto the runtime of the frontend
tests for no reason. This PR changes the `frontend:test` rule to respond
to the `COVERAGE` and `CI` env vars to enable coverage analysis of the
frontend tests if the setting tells them to.
# Description of Changes
**PR2 of the encrypt-at-rest initiative — PR1 was #7155** Makes the P1
crypto operable and compliance-credible: admins can see the feature's
state, flip the kill switch over an API instead of raw SQL, encrypt the
pre-existing plaintext backlog, rotate the master key, and every
security-relevant event lands in the audit trail. No frontend — that's
PR3.
**What was changed**
- **Audit events** — new `STORAGE_ENCRYPTION` audit type, emitted
through a small listener interface so the crypto classes stay plain
objects: `encrypt`, `decrypt` (per-read events honour
`storage.encryption.auditReads`, default **on** — HIPAA reviewers expect
read audit; busy installs can disable), `decrypt.denied` (always),
`key.created/disabled/enabled`, `master.rotated`, `migration.completed`,
plus a `plaintextExport` marker whenever a plaintext copy of
encrypted-at-rest content is served (with `inline` flag to distinguish
in-app view from saved download).
- **Admin API** `/api/v1/admin/storage-encryption` (`hasRole('ADMIN')`):
- `GET /status` — write/decrypt state, **master-key fingerprint**
(SHA-256 prefix for backup verification, never key material), encrypted
vs plaintext file counts, full key list with status history.
- `POST /keys/{id}/disable` / `enable` — the kill switch, now with
active cache invalidation so revocation is immediate on the handling
node (cross-node converges within the 60s cache TTL). Enable is
restricted to DISABLED keys so two ACTIVE keys can't exist per scope.
- `POST /migrate` + `GET /migrate/status` — encrypt-existing job.
- `POST /master/rotate` — key material is never accepted over HTTP; keys
come from config/env.
- **Deliberately no delete endpoint** — key material can be disabled but
never destroyed through the API.
- **Encrypt-existing migration job** — new writes are encrypted from the
moment the flag is on; this converts the backlog. Crash-safe per file:
store the encrypted copy under a NEW storage key → compare-and-swap the
DB row → only then delete the old blob. A CAS miss (user replaced the
file mid-run) discards the job's copy — the user's file always wins.
Worst crash outcome is an orphaned blob, never a lost file; re-runs are
idempotent (`encryption_key_id IS NULL` selection, cursor-paged so
failures can't wedge the loop). Handles all three blobs per row
(main/history/audit-log), runs on a throttled virtual thread,
single-flight guarded.
- **Master-key rotation** — cheap by design thanks to the P1 hierarchy:
rotate re-wraps the handful of KEK rows, zero file I/O. New config
`stirling.security.fileEncryptionKeyPrevious` (+env) gives `unwrap` a
fallback during rotation, and
`stirling.security.fileEncryptionKeyVersion` marks which master wrapped
each row. Runbook: set new key primary + old as previous + bump version
→ restart (startup self-check passes via fallback, warns about pending
rows) → `POST /master/rotate` → remove the previous key.
- **Shared state bean** — `StorageEncryptionState` is built once and
shared by the storage decorator and the admin API, so kill-switch cache
invalidation hits the same caches the decorator reads.
**Reviewer notes**
- The revoked→403 mapping promised for PR2 already landed in #7155 after
manual testing; this PR adds the matching `decrypt.denied` audit event.
- 19 new tests: audit emission (encrypt/decrypt/denied, legacy plaintext
emits nothing), kill-switch immediacy (no TTL wait), rotation
(previous-key fallback, re-wrap + cleanup, idempotent second call),
migration (backlog encrypted byte-identical, CAS-miss discards own copy,
per-file failure counting, concurrent-start rejection, write-disabled
rejection), admin controller status/conflict/not-found paths.
- Full proprietary suite: 2246/2247 green (the one failure is the
pre-existing Windows-symlink FolderIdentitiesTest, unrelated).
---
[ENCRYPTION_AT_REST_TEST_REPORT.html](https://github.com/user-attachments/files/30664158/ENCRYPTION_AT_REST_TEST_REPORT.html)
---------
Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
# Description of Changes
This PR modernizes the project's Python tooling across GitHub Actions by
migrating CI workflows from pip-based dependency management to `uv` and
aligning Python execution with the engine project's managed environment.
### What was changed
- Replaced `actions/setup-python` and ad-hoc `pip install` steps with
`astral-sh/setup-uv` across CI workflows.
- Configured shared `uv` dependency caching using
`engine/pyproject.toml` and `engine/uv.lock`.
- Updated Python script execution to use `uv run --project engine
--locked` for a consistent runtime environment.
- Replaced package installation steps with `uv sync` for the required
dependency groups (e.g. `tools` and `cucumber`).
- Added Docker image build validation for both production and
development AI engine images.
- Updated workflow cache configuration and Docker build context where
required.
- Removed obsolete Python requirements files that are no longer needed
after the migration.
- Applied minor Python code modernizations, including import cleanup,
modern built-in generic type annotations (`list[...]`, `tuple[...]`,
`float | None`), and small style improvements.
- Removed unnecessary Python formatter/linter extensions from the
development container configuration.
### Why the change was made
- Standardize Python dependency management across the repository.
- Reduce duplicated dependency installation logic in CI.
- Improve workflow performance through shared dependency caching.
- Ensure all Python utilities execute against the same locked dependency
set managed by the engine project.
- Simplify long-term maintenance by eliminating legacy requirements
files and pip-specific workflow steps.
---
## 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.
---------
Signed-off-by: Carsten Drewes <c.drewes@stud.uni-hannover.de>
Co-authored-by: albanobattistella <34811668+albanobattistella@users.noreply.github.com>
Co-authored-by: kastenherri <116314318+kastenherri@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: James Brunton <jbrunton96@gmail.com>
Auto-generated by stirlingbot[bot].
Regenerates `frontend/editor/src/portal/generated/docsManifest.json`
from the Stirling docs repo via `npm run docs:sync`.
Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
# Description of Changes
PR 1 of the failure-notification work: a durable, team-scoped record of
**why a policy run failed**, surfaced in the portal with the triage
actions each failure allows.
Today a failed policy run is not quite invisible, but it is unusable:
the ledger marks the file `ERROR`, and the audit aspect keeps the
exception message and status code. Nothing classifies either one,
nothing surfaces them, and neither offers a next step. If the file came
from a folder, bucket or webhook there is also no user watching, so
nobody learns it never made it through. This adds the record and the
read surface; the remediation that acts on documents comes later (see
below).
## What this does
**A failure kind registry as data.** `FailureKind` describes what can go
wrong: a stable wire id, i18n keys, an English fallback, and four facets
the review surface needs (`Stage`, `Severity`, `Remedy`, `Scope`). It is
shaped like the existing `ExceptionUtils.ErrorCode` and *links* to that
vocabulary rather than replacing it.
**Classification off structured codes, not message matching.** Policy
steps dispatch over loopback HTTP, so a tool's 4xx arrives as a
`RestClientResponseException` whose body is the Problem Details document
carrying `errorCode`. `FailureClassifier` reads that. Anything
unrecognised becomes `UNKNOWN`, which is the point: every failed run
gets an addressable record from day one, and which kinds to promote next
is answered by production frequency rather than guesswork.
**Actions declared by a kind, implemented as beans.** A kind lists the
`FailureActionId`s it offers; behaviour lives in `FailureAction` beans
resolved by id — the idiom this codebase already uses for `InputSource`,
`PolicyOutputSink` and `PolicyTrigger`. A kind cannot be sent an action
it never declared (400), so an incoherent pairing is unreachable rather
than merely unrendered. A new kind ships as a registry entry plus copy:
no new endpoint, no UI change.
**Repeat folding.** Recording folds a genuine repeat into the existing
incident instead of inserting again, keyed on `(team_id, dedup_key)`.
That matters for a snapshot-mode source that re-lists every file on each
sweep: the same broken file is one incident, not one per sweep. Distinct
files keep distinct rows. The unique constraint is enforced by the
database, and a writer that loses the insert race folds into the
winner's row.
One granularity caveat worth naming: nothing populates `file_id` in this
PR, so every row has it NULL. A FILE-scoped kind therefore dedups on
`policy + run` rather than `policy + file`. That still yields one row
per document for the sources shipped here, because the folder, S3 and
webhook sources each start one run per file; it stops holding as soon as
a single run carries several documents, which is why editor-origin
reporting (item 3 below) populates `file_id`.
**No document identity is stored.** No file name, no content. `fileId`
is an opaque reference only the owner's own client can resolve locally.
`detail` keeps the raw message (the only diagnostic an `UNKNOWN` failure
has) with anything path- or filename-shaped stripped on the way in,
capped at 2,000 characters. `PolicyExecutor`'s type-mismatch message now
reports the *extension* rather than the filename, since that message
becomes the stored `detail`.
**Access.** Reads and triage are leader-only, gated exactly the way
`PolicyController` gates policy editing, with the single-user carve-out
when login is disabled. Every read and write is scoped to the caller's
own team from the authenticated principal — there is no team parameter
on the API.
Self-hosted needs no migration: the table is created from the entity by
`ddl-auto=update`, as with every other table.
## What this does not do yet
- **Actions are incident dispositions, not document dispositions.**
Acknowledge and Dismiss change how a failure is displayed and touch
nothing else — not the document, not the processed-file ledger, not the
run, not any output destination. That is what makes them safe to offer
against `UNKNOWN`, and why there is no Approve/Release yet.
- **Two kinds only.** `INPUT_PASSWORD_PROTECTED` and `UNKNOWN`.
Everything else classifies as `UNKNOWN` and shows its raw message.
- **Editor-origin failures are not reported.** Every row is `PROCESSOR`.
`FailureOrigin.EDITOR` and `API` exist in the enum but nothing writes
them.
- **The list is dev-only for now.** The section renders behind
`import.meta.env.DEV`, so it ships in no production bundle. The
endpoints are live and gated.
- **No retention or per-team cap** on `file_run_events`. Tracked
separately.
- **No suspend-and-prompt.** `PolicyInputRequiredException` and the
engine's `suspend()` exist but nothing throws it, so a run cannot pause
to ask for a password today.
- **SaaS needs a migration** in `Stirling-PDF-SaaS` (`CREATE TABLE IF
NOT EXISTS stirling_pdf.file_run_events`), per the convention documented
at `app/saas/src/main/resources/application-saas.properties:21`.
## What follows in later PRs
1. **Map the remaining error codes to specific kinds** — corrupted file,
OCR unavailable, output destination unreachable, entitlement refusals,
and so on — each with its own copy and its own action set, replacing
today's `UNKNOWN` catch-all with a named notification in the review UI.
2. **Real remediation actions** attached to those kinds: fix (supply a
password and resume), skip (drop this file, continue the batch), and
decline (reject an incoming file outright), acting on the held document
rather than only on the incident row. This is where the
suspend-and-prompt path gets wired.
3. **Editor-origin reporting**, so a failure a user hits in the editor
lands in the same queue as one from a bucket.
4. **The user-facing review surface**: notifications with a sticky
review section, per-file badges, and an export gate, with the dev-only
list here replaced by the real thing.
## How to test
Needs a SaaS or proprietary build with login enabled, and an account
that leads a team.
1. Create a policy in the Processor with any step (Auto-redact is fine)
and a source you can drop files into.
2. Upload two files that will fail it: **a password-protected PDF**, and
**a corrupted PDF** (truncate a valid one, or rename a `.csv` to
`.pdf`).
3. Let the policy run and fail on both.
4. Go to the portal's **Documents** view and scroll to **Failures** (dev
builds only).
Expect two rows:
- **Password-protected document** — classified from `E004`, with the
kind's own labels **"I'll unlock this"** and **"Skip this file"** rather
than generic wording.
- **Unrecognised failure** — the corrupted file, classified `UNKNOWN`
(`E001` is not claimed by a kind yet), showing its raw message with
generic **Acknowledge** / **Dismiss**.
Neither row contains a file name anywhere, including in the raw message.
Press **Show raw JSON** to read exactly what the server returned. Acting
on a row transitions it and comes back with both buttons disabled and a
reason.
Re-running the same batch increments the occurrence count on the
existing rows rather than adding new ones; two *different*
password-protected files produce two separate rows.
---
## 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
# Description of Changes
> Stacked on #7264, sibling of #7283. Independent of #7283 — the only
overlap is two additive lines in `core/query/keys.ts` and
`core/api/config.ts`. Either can merge first.
## The problem
`useEndpointConfig` kept its own cache: a module-level `globalFetchDone`
boolean, a mutable `globalEndpointCache` object, and a
`resetGlobalCache()` called from the JWT listener. Which consumer
mounted first decided who paid for the request, and nothing invalidated
it except a page reload.
## End state
One shared query for the whole availability map; each of the 12
consumers projects the endpoints it asked for. **251 lines to 101**,
same return shape, no consumer changes.
| | Before | After |
|---|---|---|
| Cross-consumer cache | `globalFetchDone` + mutable module object |
query key |
| Invalidation | `resetGlobalCache()` mutating that object |
`invalidateQueries` |
| Per-endpoint check | own `useState` triple | query keyed by endpoint |
Behaviour kept deliberately:
- **Unknown endpoints and any failure still read as enabled.** This
fires before auth settles, and disabling every tool on a hiccup is worse
than letting one call fail later.
- **`retry` is off for the availability map.** The fallback *is* the
answer, so retrying only doubles a request every logged-out visitor
makes on load.
## Desktop is untouched
`desktop/hooks/useEndpointConfig.ts` shadows this module entirely — no
shared code, so core converting doesn't affect it and there's no
half-migrated state.
It's 482 lines of orchestration rather than fetching: dependency-ready
gating, `tauriBackendService` and `selfHostedServerMonitor`
subscriptions, a 2.5s timeout retry for backend startup, a legacy
`?endpoints=` fallback for old servers, and SaaS-routing optimism that
rewrites disabled endpoints to enabled. It also has no test coverage to
convert against, and it decides whether tools appear at all in the
desktop app.
That's a different job from this one and wants its own review. Next PR.
## Testing
9 new tests: projection onto the requested subset, one request across
consumers, unknown-endpoint fallback, failure fallback with no retry,
empty-list no-fetch, JWT invalidation, and the three single-endpoint
cases.
`task frontend:check` green: 1677 tests across 192 files, typecheck on
all five flavours, eslint, dpdm, prettier.
Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
# Description of Changes
Fix automate unrunnable tools
## Problem
- Remove Image failed in Automate with `Tool operation not supported:
removeImage`
- Its registry entry had `operationConfig: undefined` even though the
config existed and was already tested
- The Automate picker only filtered on `supportsAutomate`, never on
`operationConfig` — so broken tools were selectable and failed only at
run time
## Fixes
- Wire up `removeImage` and `pageLayout` operation configs (both already
existed, just never registered)
- Exclude `validateSignature` (report tool, not on the operationConfig
seam) and `scannerEffect` (no frontend implementation) via
`supportsAutomate: false`
- Picker now also filters on `operationConfig`, so this class of bug
can't reach users again
- `overlay-pdfs` returns 400 instead of 500 when overlay files or mode
are missing
- Fix `new URL().pathname` Windows path bug that stopped 2 test suites
from loading
---
## 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.
## What
Makes Storybook render components with the same CSS the app gives them.
- The preview loaded the token primitives but **not the editor's
semantic token layer** (`styles/theme.css`), so components styled on
those variables rendered unthemed — three onboarding stories were
importing it by hand to stop their modal surfaces rendering transparent.
It's now loaded in the preview and the workarounds are gone.
- **Portal stories render inside the `.portal-scope` wrapper** PortalApp
mounts, so the portal's scoped reset and typography apply to them
exactly as in the app — and, deliberately, to nothing else.
- The folder stories invented their own hex colours, two of which aren't
values the app's `FOLDER_COLOR_PALETTE` can produce. They now use the
palette, so they can't drift from what a user can actually pick.
Deliberately does **not** load `tailwind.css` — tailwind is on its way
out of the editor, so matching the token layer alone is the target
state.
## Story colours route through the tokens, enforced
Stories were exempt from the `code-colors` lint, and it showed:
hardcoded hexes for surfaces the tokens already name (chat bubbles,
borders, demo backgrounds), `var(--x, #hex)` fallbacks that mask a
renamed token by silently painting the stale colour, and mocked category
accents for which real `--color-cat-*` tokens exist.
- Styling literals now use tokens; the dead fallbacks are stripped.
- The stories exemption is removed from `theme-lint`, so this can't
regress.
- Colours that are **the datum itself** — `ColorInput` values, signature
ink, per-policy accents, brand-mark swatches — stay literal via
`theme-allow-color`, hoisted to named consts so the exemption and its
reason sit together.
A practical side effect: stories styled on tokens actually respond to
the dark-mode toolbar toggle, which is what makes a dark-theme a11y pass
meaningful later.
## The a11y gate now runs dark as well as light
Contrast is most of what axe reports and it is theme-dependent, so a
light-only gate left half the surface unmeasured — and it only becomes
measurable at all once the tokens above actually flip. `SCAN_THEME=dark`
pins the theme for a whole scan run, every a11y task runs both themes,
and each theme has its own baseline:
- **light** re-recorded against the themed rendering (the old baseline
measured colours the app never shows): 831 stories with violations
- **dark** recorded for the first time: 798 stories with violations, 980
story-rule pairs, zero render failures across the full sweep
Verified end to end: dark scans measure against dark surfaces (`#18181b`
vs `#ffffff`), both baselines self-check clean, and a live scan of
stories that changed on main after recording passes both gates.
Nightly's timeout doubles for the second sweep.
## Testing
Typecheck (all variants), ESLint and Prettier pass. Onboarding, folder,
portal and control stories render in the browser scan (39/39) with the
per-story CSS imports removed; every story touched by the colour sweep
renders too (58/58). `task frontend:lint:colors` passes with stories
included.
# Description of Changes
Currently when calculating the output file type for some tools, the
system will get it wrong because it doesn't know about what the default
parameters in tools are, so if it doesn't have a value for some key,
it'll just bail out and say "it might not be compatible". This PR adds
logic to `ToolIO` to read the default values set for the parameters if
the tool has `ToolIOCase`s and takes them into account when figuring out
the output type. I've built it with horrible Java reflection magic to
avoid having to specify the default for params twice, which will make it
impossible for the defaults to disagree with each other. This just runs
once at startup so there's negligible performance impact.
The change is easily tested with Change Parameters, which is just
`add-password` behind the scenes but with the password params omitted
(so Change Password is always PDF->PDF, never encrypted like Add
Password).
Also (somewhat hackily) fixes a bug I noticed where saving a Change
Permissions step then leaving and returning to the pipeline will cause
the step to be reloaded as Add Password. I've added a system to
disambiguate tools which share the same endpoint (which is only these
two currently).
## Currently
<img width="455" height="135" alt="image"
src="https://github.com/user-attachments/assets/c8867d6a-599b-4f21-a2db-4a1b6ac22d73"
/>
## Now
<img width="415" height="122" alt="image"
src="https://github.com/user-attachments/assets/bd8d1bab-f00a-421b-8c91-5af0d2ad5335"
/>
# Description of Changes
Nightlies keep failing because the Playwright tests only run on Chrome
in PRs. This PR changes it so that we run all 3 browsers in all
(frontend) PRs so we catch these things before they merge in. They run
in parallel so it won't take any more time for the CI to finish.
# Description of Changes
Continued effort towards removing all uses of the any type in our
frontend code (last PR was #7326). This PR fixes 7 more folders and
removes them from the exclude list. All of them were localised within
the folder in the exclude list so again were pretty easy to fix.
If you have any additional information, comments, or resources you think would support or be relevant to your feature request, include them here.
- type:textarea
id:sample-files
attributes:
label:Example Files
description:|
If the feature request depends on specific PDFs or other example files, attach them here when available. Remove any sensitive information before sharing.
- [ ] 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
- [ ] Every comment I added says something the code does not ([guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/CODE_COMMENTS.md))
const praise = `## 🤖 AI PR Title Suggestion\n\nGreat job! The current PR title is clear and well-structured.\n\n✅ No suggestions needed.\n\n---\n*Generated by GitHub Models AI*`;
if (existing) {
await github.rest.issues.updateComment({
owner, repo,
comment_id: existing.id,
body: praise
});
console.log("Replaced suggestion with praise.");
} else {
console.log("Rating > 5 and no existing comment – skipping comment.");
}
}
- name:is not repo dev
if:steps.actor.outputs.is_repo_dev != 'true'
run:|
exit 0 # Skip the AI title review for non-repo developers
@@ -21,6 +21,43 @@ Task `desc:` fields should describe **what** the task does, not **how** it does
-`task docker:build` — build standard Docker image
-`task docker:up` — start Docker compose stack
## Comments
A comment must carry information the code cannot. If a reader could derive it from the code in front of them, delete it.
Comment the current state. Not what the code used to do, not what changed, not why it changed: git holds that. Where history explains the shape, state the reason instead, so "this used to reimplement the modal internals" becomes "thin wrapper over the shared Modal: duplicating its portal and focus trap is how dialogs drift apart". Future state goes in a TODO with an issue.
Write a comment when it does one of these four jobs:
- **Contract.** What a caller must know that the signature cannot say: preconditions, invariants, units, ownership and lifetime, thread-safety, error semantics, side effects. Document the contract of everything a caller outside the file can reach, and nothing else. Goes on the type/method/module as Javadoc, JSDoc, or a docstring.
- **Why.** The constraint the code satisfies, the bug it avoids, the alternative rejected and the reason.
- **Hazard.** "Must stay in sync with X", "order matters because Y", "do not remove, it prevents Z".
- **Map.** A short orientation at the top of a genuinely complex file: what it owns, and what it deliberately does not.
Never write:
- A comment that restates the next line. `// Handle drag start` above `handleDragStart` is noise.
- Section banners or position markers: `// --- Types ---`, `// Helpers`, `// =====`.
- Step narration in a function body (`// Step 1:`, `// Then we`). If the steps need labels they need names: extract functions. Numbering a genuinely numbered thing, like a wizard step, is fine.
- Commented-out code. Delete it.
- Doc tags that restate the signature. `@param blob - The blob` says nothing; omit the tag rather than pad it.
- Docs on self-explanatory members with no constraint to state.
Two tests before keeping a comment:
- **Delete it.** Is any information lost? If not, it stays deleted.
- **Could a name carry it instead?** A better identifier, an extracted function, or a named constant beats a comment. Prefer the code change.
A comment at the end of a line usually decodes that line, and that is worth keeping: `{0x25, 0x50} // "%PDF"`, `50L * 1024 * 1024 // 50 MB`. The rules that compare a comment against the code below it do not apply there, but a trailing TODO or a trailing bit of history is judged like any other.
A reference is supplementary, never load-bearing: the comment must survive deleting it. `// See #1234` is a dead end; `// saving first loses every annotation (#6865)` is not. Prefer a spec (`RFC 3161`) or CVE where one applies.
A TODO needs an issue, not an owner: `// TODO(#1234): re-enable the gate once account syncing lands`. If it is not worth an issue, it is not worth a TODO. A question is not a TODO.
A comment block over ~12 lines outside a file or type header usually means the code needs restructuring, or that the prose is product documentation and belongs in the docs repo.
`task comment-lint` checks the mechanical part of this on the lines you add, and runs inside `task pre-commit`. Reasoning, worked examples and the linter's own rules: @devGuide/CODE_COMMENTS.md
## Common Development Commands
### Build and Test
@@ -70,7 +107,7 @@ The project structure is defined in `engine/pyproject.toml`. Any new dependencie
- Avoid nested functions and nested classes unless the language construct requires them.
- Prefer composition to inheritance when combining concepts.
- Avoid speculative abstractions. Add a layer only when it removes real duplication or clarifies lifecycle.
-Add comments sparingly and only when they explain non-obvious intent.
-Comments follow the repo-wide rules in the "Comments" section above.
#### Python Typing and Models
- Deserialize into Pydantic models as early as possible.
Thank you for your interest in contributing to Stirling-PDF! There are many ways to contribute other than writing code. For example, reporting bugs, creating suggestions, and adding or modifying translations.
## License
By contributing to this project, you agree that your contributions will be licensed under the project [license](LICENSE), which follows an open-core model.
The codebase is a mix of MIT and source-available code, so your contribution is licensed according to the directory it is committed to.
PRs are welcome in any directory by any user, just be aware of which license applies to the code you change.
## Issue Guidelines
Issues can be used to report bugs, request features, or ask questions. If you have a question, you could also ask us in our [Discord](https://discord.gg/FJUSXUSYec).
@@ -35,6 +42,7 @@ Please make sure your Pull Request adheres to the following guidelines:
- Keep commits atomic. One commit should contain one change. If you want to make multiple changes, submit multiple Pull Requests.
- Commits should be clear, concise, and easy to understand.
- References to the Issue number in the Pull Request and/or Commit message.
- Every comment in the diff should say something the code does not. See [Code comments](devGuide/CODE_COMMENTS.md); `task comment-lint` checks the mechanical part.
## Translations
@@ -63,7 +71,3 @@ For technical guides, setup instructions, and development resources:
For configuration and usage guides, see:
- [Database Guide](DATABASE.md) - Database setup and configuration
- [OCR Guide](HowToUseOCR.md) - OCR setup and configuration
## License
By contributing to this project, you agree that your contributions will be licensed under the [MIT License](LICENSE).
Measured by diffing the OpenAPI document of a booted `origin/main` (Spring, proprietary flavor)
against a booted branch jar of the same flavor.
- **The automation/policy stack is unavailable on the proprietary flavor.** Every policy bean
carries `@IfBuildProfile("saas")` - a pre-existing branch decision, not something main does:
main serves policies on proprietary. Arc turns that gate into `@Vetoed` off the saas profile, so
each consumer needs the same gate or augmentation fails; 11 controllers/services were gated to
make the flavor build. The visible symptom in the spec diff is one missing operation,
`GET /api/v1/admin/settings/policies/implied-folder-roots`, but the whole subsystem is off.
Deciding whether policies should run on proprietary is an owner call, not a mechanical port.
- **`policies.streamTimeoutMs` is ignored.** `/api/v1/policies/run-stream` used Spring's
`SseEmitter(timeout)`; JAX-RS SSE has no per-sink deadline, so the stream is now bounded by the
container's HTTP idle timeout instead of the configured 30 minutes.
- **Multipart parameters no longer bind from the query string.** Spring's `@RequestParam` read
both the query string and the form body; `@RestForm` reads only the body. 20 operations are
affected. A client that passed these as query parameters on a multipart POST would now get a
null. The frontend sends them as form fields, so this is a compatibility narrowing for API
callers rather than a broken feature.
Everything else in the spec diff is benign: 97 operations where main published an opaque request
body and the branch now describes the individual multipart fields, 20 same-name relocations from
the change above, and 40 branch-only routes (static/SPA paths, MCP, mobile-scanner, AI) that
springdoc did not document. **No operation loses a parameter outright.**
## Deferred behaviour
374 notes were removed from the source and recorded here. These are places that compile but
where the behaviour is a stub, a fallback, or a Spring feature that was dropped rather than
ported - so they will not show up in a build and need reading before anyone trusts the
corresponding feature. Grouped by concern.
<details><summary><b>Spring MVC handler registry has no Quarkus equivalent</b> (8)</summary>
-`app/common/src/main/java/stirling/software/common/config/swagger/ToolIOOperationCustomizer.java:38` - GlobalOpenApiCustomizer}, which received the {@code HandlerMethod} for each operation and could read {@code@ToolIO} straight off it. A MicroProfile {@link OASFilter} sees only the document, so the declarations are looked up by path through {@link ToolIORegistry}. That registry is only populated once the container is up, hence {@code RUNTIME_STARTUP} - the schema exported at build time by {@code quarkus.smallrye-openapi.store-schema-directory} therefore ...
-`app/core/src/main/java/stirling/software/SPDF/config/EndpointInspector.java:32` - this previously used Spring MVC's RequestMappingHandlerMapping (org.springframework.web.servlet.mvc.method.*) to enumerate all registered GET handler mappings via the ApplicationContext at ContextRefreshedEvent. Quarkus/JAX-RS (RESTEasy Reactive) has no equivalent runtime-queryable handler-mapping registry. Options for porting: - Build-time scan of @jakarta.ws.rs.Path + @jakarta.ws.rs.GET via a Quarkus build step / Jandex index ...
-`app/proprietary/src/main/java/stirling/software/proprietary/mcp/catalog/McpToolCatalog.java:85` - endpoint discovery relied on Spring MVC's RequestMappingHandlerMapping (ApplicationContext.getBeansOfType(...) -> mapping.getHandlerMethods()) to enumerate every @RequestMapping/@PostMapping handler, its URL patterns (RequestMappingInfo#getDirectPaths), its HTTP methods (RequestMethod POST/PUT), and the HandlerMethod/MethodParameter reflection used to build request schemas. Quarkus/RESTEasy Reactive has no equivalent runtime ...
-`app/proprietary/src/main/java/stirling/software/proprietary/mcp/catalog/McpToolCatalog.java:113` - request body type was previously resolved from Spring's HandlerMethod#getMethodParameters(); resolve the first complex parameter type via plain reflection on the JAX-RS resource method instead, then call schemaGenerator.toSchema(...).
-`app/proprietary/src/main/java/stirling/software/proprietary/mcp/catalog/OperationMeta.java:16` - was org.springframework.web.method.HandlerMethod (Spring MVC, no Quarkus equivalent). Replaced with the underlying java.lang.reflect.Method. The collaborator McpToolCatalog must be updated to discover JAX-RS resource methods (e.g. via RESTEasy Reactive ResourceScanningSupport / jakarta.ws.rs annotations) instead of Spring's RequestMappingHandlerMapping, and pass a reflect.Method here.
-`app/proprietary/src/main/java/stirling/software/proprietary/service/AiEngineEndpointResolver.java:44` - this previously enumerated all registered request mappings via Spring MVC's RequestMappingHandlerMapping (org.springframework.web.servlet.mvc.method.*) obtained from the ApplicationContext at ContextRefreshedEvent, keeping every pattern that started with "/api/v1/". Quarkus / JAX-RS (RESTEasy Reactive) has no equivalent runtime-queryable handler-mapping registry. Options for porting: - Build-time scan of @jakarta.ws.rs.Path ...
-`app/saas/src/main/java/stirling/software/saas/payg/entitlement/EntitlementGuard.java:366` - resolves the resource {@link Method} the original code read from Spring's {@code HandlerMethod}. Until wired to JAX-RS {@code ResourceInfo}, supports a handler that is already a {@link Method} or exposes a no-arg {@code getMethod()} returning one.
-`app/saas/src/main/java/stirling/software/saas/payg/filter/PaygChargeInterceptor.java:563` - resolves the resource {@link Method} the original code read from Spring's {@code HandlerMethod} (via {@code hm.getMethod()}). Until wired to JAX-RS {@code ResourceInfo}, supports a handler that is already a {@link Method} or exposes a no-arg {@code getMethod()} returning one, preserving the {@code@AutoJobPostMapping} gating.
-`app/common/build.gradle:11` - Servlet bridge: large amounts of controller/filter code use jakarta.servlet (HttpServletRequest, Filter, etc.). quarkus-undertow provides a servlet container on Quarkus so that API resolves and runs. longer term, port servlet usage to JAX-RS (ContainerRequestContext) and drop quarkus-undertow.
-`app/common/build.gradle:27` - REMOVED: spring-boot-starter-aspectj. Quarkus has no AspectJ weaving; quarkus-arc provides CDI interceptors (@AroundInvoke / interceptor bindings) instead. any @Aspect/@Around advice must be rewritten as CDI interceptors.
-`app/core/src/main/java/stirling/software/SPDF/config/LocaleConfiguration.java:11` - this class was a Spring MVC WebMvcConfigurer. Quarkus/JAX-RS has no WebMvcConfigurer, InterceptorRegistry, LocaleChangeInterceptor or SessionLocaleResolver. The locale-resolution logic (computing the default Locale from configuration) is preserved below as a CDI-produced Locale. The two pieces of behavior that previously came from the MVC machinery still need to be wired up by collaborators: 1. The "lang" request-param locale ...
-`app/core/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java:38` - are read automatically, so this class is now an {@link OASFilter} (registered via {@code mp.openapi.filter} in application.properties) that reproduces the old programmatic customizations: <ul> <li>API {@link Info} (title, version, license, contact, terms of service, description); <li>the global "AI" {@link Tag}; <li>the {@link Server} entry (optionally from {@code SWAGGER_SERVER_URL}); <li>the {@code ErrorResponse} component schema; <li>the {@code apiKey} ...
-`app/core/src/main/java/stirling/software/SPDF/config/SpringDocConfig.java:3` - springdoc's GroupedOpenApi (multiple OpenAPI documents grouped by path-matching) has NO direct equivalent in quarkus-smallrye-openapi, which serves a single document built automatically from @Tag/@Operation/JAX-RS annotations. The three groups below (file-processing "/api/v1/**" minus management/system paths, management "/api/v1/admin/**" etc., and system "/api/v1/ui-data/**" etc.) plus the pdfFileOneOfCustomizer ...
-`app/core/src/main/java/stirling/software/SPDF/config/WAUTrackingFilter.java:21` - Spring @ConditionalOnProperty(name="security.enableLogin", havingValue="false") had no direct CDI equivalent for conditional bean registration. The filter is now always registered (@Provider) and the condition is enforced at request time by reading the 'security.enableLogin' config property below. Verify the property key matches Quarkus config (originally bound from ApplicationProperties.security.enableLogin).
-`app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java:63` - in Spring, addResourceHandlers also registered the physical resource locations (InstallationPathConfig.getStaticPath() + "classpath:/static/") and an EncodedResourceResolver (gzip/brotli pre-compressed asset serving). In Quarkus, static file serving is handled by quarkus.http via configuration: quarkus.http.static-resources... and/or a Servlet/RouteFilter mapping InstallationPathConfig.getStaticPath() as an external static root ...
-`app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java:152` - Quarkus has built-in CORS handling via quarkus.http.cors.* config properties (quarkus.http.cors.origins, .methods, .headers, .exposed-headers, .access-control-allow-credentials, .access-control-max-age). However, the original logic is *dynamic* (Tauri-mode detection + ApplicationProperties-driven origins + always-on Tauri origins), which static config cannot express. The logic is preserved below and applied via this response ...
-`app/core/src/main/java/stirling/software/SPDF/exception/GlobalExceptionHandler.java:106` - the per-request locale used to come from Spring's LocaleContextHolder (populated by the MVC LocaleChangeInterceptor). Until the equivalent ContainerRequestFilter described in LocaleConfiguration is in place, fall back to the JVM default locale. Localized messages are read from the shared messages.properties bundle (the same bundle ExceptionUtils uses) instead of a Spring MessageSource bean, which no longer exists under Quarkus.
-`app/core/src/main/java/stirling/software/SPDF/exception/GlobalExceptionHandler.java:806` - the original Spring handler checked HttpServletResponse.isCommitted() and returned null to let Spring write nothing when the response was already committed (e.g. during streaming). JAX-RS ExceptionMapper has no direct access to commit state; returning a Response here is the closest equivalent. If streaming endpoints need the old "do nothing when committed" behavior, a collaborator should detect that condition (e.g. via a ...
-`app/core/src/main/java/stirling/software/SPDF/exception/GlobalExceptionHandler.java:873` - locale is the JVM default until the per-request locale ContainerRequestFilter described in LocaleConfiguration replaces Spring's LocaleContextHolder.getLocale().
-`app/core/src/main/resources/application.properties:46` - no direct Quarkus equivalent for the following; handle in code: - spring.threads.virtual.enabled=true -> annotate blocking endpoints with @RunOnVirtualThread - spring.mvc.async.request-timeout -> per-endpoint timeout handling - spring.security.filter.dispatcher-types=REQUEST,ERROR - spring.web.resources.mime-mappings.webmanifest=application/manifest+json - server.servlet.session.tracking-modes=cookie (configure on ...
-`app/proprietary/src/main/java/stirling/software/proprietary/audit/AuditAspect.java:34` - {@code@Around("@annotation(...Audited)")} advice. Reworked into a CDI {@link Interceptor} bound by the {@code@Audited} annotation; {@code@Around}/{@code ProceedingJoinPoint} became {@code@AroundInvoke}/{@link InvocationContext}. Spring's {@code@Order(10)} (lower precedence, runs after {@code AutoJobAspect}) maps to {@code@Priority}: {@code AutoJobAspect} uses {@code@Priority(20)}, so this audit interceptor uses {@code@Priority(10)} which runs ...
-`app/proprietary/src/main/java/stirling/software/proprietary/audit/AuditAspect.java:41` - stirling.software.proprietary.audit.Audited}) must be made a CDI {@code@jakarta.interceptor.InterceptorBinding} (and its members marked {@code@jakarta.enterprise.util.Nonbinding}) for this {@code@Interceptor} to bind to it; see the already-migrated {@code AutoJobPostMapping}. That is a separate file and is intentionally left untouched here. {@code AuditService}'s helper methods ({@code createBaseAuditData}, {@code ...
-`app/proprietary/src/main/java/stirling/software/proprietary/audit/ControllerAuditAspect.java:38` - multiple {@code@Around} advice whose pointcuts matched <em>any</em> method annotated with Spring's {@code@GetMapping}/{@code@PostMapping}/{@code@PutMapping}/{@code@DeleteMapping}/ {@code@PatchMapping}/{@code@AutoJobPostMapping}, plus an {@code execution(...)} expression on Spring's {@code ResourceHttpRequestHandler}. {@code@Around}/{@code ProceedingJoinPoint} + {@code MethodSignature} became {@code@AroundInvoke}/{@link InvocationContext}, and ...
-`app/proprietary/src/main/java/stirling/software/proprietary/audit/ControllerAuditAspect.java:204` - (collaborator) - AuditService.createBaseAuditData/addFileData/ addMethodArguments/resolveEventType still take org.aspectj.lang.ProceedingJoinPoint (AuditService is not yet migrated). Once AuditService is converted, change those signatures to accept jakarta.interceptor.InvocationContext (getMethod/getParameters/ getTarget cover the data used). These calls pass the InvocationContext and will only typecheck after that collaborator ...
-`app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpApiKeyAuthFilter.java:42` - Spring Security removed. This filter previously read the current Authentication from SecurityContextHolder to decide whether to process the API key. Quarkus has no SecurityContextHolder; the current identity is exposed via io.quarkus.security.identity.SecurityIdentity. With the binding below not yet wired, we always attempt to validate the presented key so the lookup logic is preserved.
-`app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpApiKeyAuthFilter.java:51` - bind the resolved user + MCP_SCOPES to the request identity. Spring's UsernamePasswordAuthenticationToken / SecurityContextHolder.setContext(...) has no servlet-filter equivalent in Quarkus. Implement an io.quarkus.security.identity.SecurityIdentityAugmentor (or a custom io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism / IdentityProvider keyed off the X-API-KEY / Bearer credential) that produces a ...
-`app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpAudienceValidator.java:14` - RFC 8707 audience binding: a JWT at the MCP endpoint must list this server's resource id (or one of the explicitly accepted additional audiences) in its {@code aud} claim. The additional list exists for IdPs that cannot mint resource-specific audiences - e.g. Supabase's OAuth server always issues {@code aud=authenticated}. Fails closed when nothing is configured. this was a Spring Security {@code OAuth2TokenValidator<Jwt>} ...
-`app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpAuthenticationEntryPoint.java:17` - Emits 401 + {@code WWW-Authenticate: Bearer resource_metadata="..."} (RFC 9728) from X-Forwarded-* headers. A rejected token also logs the reason and echoes it as {@code error_description}. this was a Spring Security {@code AuthenticationEntryPoint} (commence(...) invoked by the SecurityFilterChain on authentication failure). Quarkus has no SecurityFilterChain equivalent. The 401 response must instead be produced by a Quarkus ...
-`app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpRequestSizeFilter.java:27` - this filter was a Spring OncePerRequestFilter; under Quarkus (quarkus-undertow) register it as a jakarta.servlet.Filter via @WebFilter or a programmatic FilterRegistrationBean equivalent, and ensure it runs once per request and before the MCP endpoint. Registration ordering must be verified by the collaborator wiring the servlet filters.
-`app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpSecurityConfig.java:15` - MCP security chain: validates JWTs (JWKS + RFC 8707 audience), maps scope claims to authorities, and fails closed when the issuer is unset. this class was a Spring Security {@code SecurityFilterChain} / {@code HttpSecurity} DSL configuration, which has NO direct Quarkus equivalent. The Spring security DSL has been removed; the equivalent behaviour must be rebuilt on Quarkus primitives: <ul> <li>HTTP path matching ({@code /mcp} ...
-`app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpSecurityConfig.java:57` - @Order(Ordered.HIGHEST_PRECEDENCE) and @ConditionalOnProperty(name = "mcp.enabled", havingValue = "true") were removed. Gate MCP security wiring on the runtime property mcp.enabled=true (a runtime toggle, not a build profile, so prefer a runtime guard in the new ContainerRequestFilter/augmentor). Filter ordering (highest precedence) must be re-expressed via JAX-RS @Priority or quarkus.http.auth.permission ordering.
-`app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpSecurityConfig.java:66` - UserService was injected @Lazy to break a circular wiring with the security chain. With the Spring chain removed, inject it directly into the new API-key / user-binding ContainerRequestFilters instead of holding it here.
-`app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpUserBindingFilter.java:26` - Binds an MCP-validated JWT to a provisioned Stirling user: optionally rejects subjects with no enabled account, then rebinds the principal to the canonical Stirling username (scope authorities only) so audit/metering attribute correctly. this was a Spring Security {@code OncePerRequestFilter} that read and rewrote the {@code SecurityContextHolder} ({@code JwtAuthenticationToken}/{@code Jwt}). Quarkus has no global mutable ...
-`app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpUserBindingFilter.java:60` - extract the validated JWT and its claims from the Quarkus SecurityIdentity / JsonWebToken instead of Spring's SecurityContextHolder. The block below preserves the original binding logic but cannot run until that wiring exists, so for now every request passes through untouched.
-`app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpUserBindingFilter.java:66` - read the claim value from the validated token, e.g. jsonWebToken.getClaim(usernameClaim). Placeholder keeps the surrounding logic intact.
-`app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpUserBindingFilter.java:98` - rebind to the Stirling username, carrying only the OAuth scope authorities. With quarkus-oidc/smallrye-jwt this is done by a SecurityIdentityAugmentor that returns a new SecurityIdentity whose principal name is boundUsername and whose roles are the original token scopes. boundUsername is computed above and ready to feed into that augmentor.
-`app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpUserBindingFilter.java:116` - on the Quarkus path, rejection should clear/deny the SecurityIdentity (augmentor throws AuthenticationFailedException) or the ContainerRequestFilter should abortWith(Response.status(403)...). The 403 JSON body below is preserved as the intended response shape.
-`app/proprietary/src/main/java/stirling/software/proprietary/repository/PersistentAuditEventRepository.java:333` - --------------------------------------------------------------------- Multi-value queries for filtering by multiple types and/or principals callers must adapt to the PanacheQuery return type (see class doc). ---------------------------------------------------------------------
-`app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationFailureHandler.java:21` - this class extended Spring Security's SimpleUrlAuthenticationFailureHandler and was wired into the form-login SecurityFilterChain. Quarkus has no direct equivalent for an AuthenticationFailureHandler. The login-failure flow (lockout, bad credentials, oauth2 errors, disabled users) must be re-hosted on a Quarkus authentication mechanism - typically a custom form-auth (quarkus.http.auth.*) or quarkus-oidc - with the redirect ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationSuccessHandler.java:24` - this class previously extended Spring Security's SavedRequestAwareAuthenticationSuccessHandler, which is part of the Spring Security form-login filter chain (RedirectStrategy + SavedRequest from the HttpSession). Quarkus has no direct equivalent: post-login redirects are handled by quarkus-oidc / form-auth (quarkus.http.auth.form.landing-page, .location-cookie) or by a custom jakarta.servlet.Filter / ContainerRequestFilter / ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationSuccessHandler.java:89` - "SPRING_SECURITY_SAVED_REQUEST" was populated by the Spring Security RequestCache. Without the Spring filter chain this attribute is never set, so this branch always falls through to the home-page redirect. The original-destination redirect must be reimplemented via the Quarkus form-auth location cookie or a custom request cache.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/JwtAuthenticationEntryPoint.java:9` - this was a Spring Security AuthenticationEntryPoint (org.springframework.security.web.AuthenticationEntryPoint). Quarkus has no direct AuthenticationEntryPoint SPI; unauthenticated-access handling is wired via quarkus.http.auth.* policies and an AuthenticationFailedException mapper / a jakarta.ws.rs.ext.ExceptionMapper<io.quarkus.security.UnauthorizedException> (or a ContainerRequestFilter). The response-shaping logic below is ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/config/EnterpriseEndpointAspect.java:23` - MIGRATION (Spring AOP -> CDI interceptor): was an {@code@Aspect} {@code@Component} with {@code@Around} advice matching {@code@annotation(EnterpriseEndpoint)} / {@code@within(EnterpriseEndpoint)}. Reworked into a CDI {@link Interceptor} bound by the {@code@EnterpriseEndpoint} annotation (pattern: common/aop/AutoJobAspect). {@code@Around} + {@code ProceedingJoinPoint} became {@code@AroundInvoke} + {@link InvocationContext}; {@code ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/config/PremiumEndpointAspect.java:20` - MIGRATION (Spring AOP -> CDI interceptor): was an {@code@Aspect} with {@code@Around} advice on the {@code@PremiumEndpoint} pointcut ({@code@annotation || @within}). Reworked into a CDI {@link Interceptor} bound by the {@code@PremiumEndpoint} {@code@InterceptorBinding}; {@code@Around}/{@code ProceedingJoinPoint} became {@code@AroundInvoke}/{@link InvocationContext}. The Spring {@code ResponseStatusException(HttpStatus.FORBIDDEN, ...)} became a ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/ProprietaryWebMvcConfig.java:10` - Spring MVC's WebMvcConfigurer / InterceptorRegistry has no Quarkus (JAX-RS / RESTEasy Reactive) equivalent, so this registration class cannot be ported directly.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/ProprietaryWebMvcConfig.java:30` - the interceptor registration below was removed: registry.addInterceptor(participantRateLimitInterceptor) .addPathPatterns("/api/v1/workflow/participant/**"); Re-implement as a JAX-RS ContainerRequestFilter bound to that path (see class javadoc).
-`app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java:31` - Security configuration migrated from a Spring {@code@Configuration}/{@code@EnableWebSecurity} class to a Quarkus CDI bean. This class was built entirely around the Spring Security {@code HttpSecurity} DSL and {@code SecurityFilterChain} beans, which have NO direct Quarkus equivalent. The HTTP security model must be re-expressed declaratively/imperatively: <ul> <li><b>HTTP path policies / authorization</b> (the {@code ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java:95` - reusable, non-Spring helper logic (CORS values, X-Frame-Options decision, firewall char patterns, filter/repository factories) is retained as plain methods/producers below. this bean was {@code@DependsOn("runningProOrHigher")} and {@code@Profile("!saas")}. The dependency ordering is approximated by injecting the {@code runningProOrHigher} flag; the {@code !saas} profile gate maps to a Quarkus build profile - use {@code ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java:176` - Reusable CORS settings preserved from the original {@code corsConfigurationSource()} bean. the Spring {@code CorsConfigurationSource}/ {@code UrlBasedCorsConfigurationSource} types are removed. Apply these values via {@code quarkus.http.cors.*} in {@code application.properties} (origins, methods, headers, exposed-headers, access-control-allow-credentials=true, access-control-max-age=PT1H) or a {@code ContainerResponseFilter} ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java:230` - Resolves the desired X-Frame-Options header value, preserving the original decision logic. apply the returned value via a response filter or {@code quarkus.http.header} config (Spring's {@code HeadersConfigurer} is gone).
-`app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java:253` - samlFilterChain/filterChain/configureSecurity built the Spring SecurityFilterChain instances. Their behaviour is summarised in the class javadoc and must be reimplemented via Quarkus HTTP auth config + filters/IdentityProviders. The full original DSL is preserved in version control. No fabricated SecurityFilterChain is produced here.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java:262` - Produces the IP rate-limiting filter (plain {@code jakarta.servlet.Filter}, not a Spring-specific type, so it remains a CDI producer). registration/ordering must be handled by quarkus-undertow ({@code@WebFilter}) or a {@code ContainerRequestFilter}. This filter was already disabled in the original chain (limit is effectively a no-op at 1,000,000) pending conversion.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java:289` - JwtAuthenticationFilter is @ApplicationScoped with CDI field injection; CDI manages it directly. The @Produces factory was removed because constructing it here with explicit args is incompatible with how the bean is declared. Inject JwtAuthenticationFilter directly wherever it is needed.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AuthController.java:328` - SecurityContextHolder.clearContext() has no Quarkus equivalent; SecurityIdentity is request-scoped and not cleared imperatively. Cookie/ token invalidation is handled by the JWT cookie being dropped by the client/filter.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/filter/EnterpriseEndpointFilter.java:21` - Spring's OncePerRequestFilter has no Quarkus equivalent; implementing jakarta.servlet.Filter directly. Registered via @WebFilter (quarkus-undertow). The single-execution-per-request guarantee OncePerRequestFilter provided is effectively given for top-level servlet filters here. if this filter must run before/after other filters, ordering is not expressed by @WebFilter; configure quarkus.http.filter.* or a ServletExtension if ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/filter/JwtAuthenticationFilter.java:47` - registration/ordering. As a Spring OncePerRequestFilter this ran once per request at a Spring-defined position in the security filter chain. On Quarkus (quarkus-undertow) a jakarta.servlet.Filter needs explicit registration and ordering (e.g. a @WebFilter with urlPatterns, or a FilterRegistrationBean-style producer). Confirm this filter is registered ahead of the resource layer and that the once-per-request semantics are ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/filter/JwtAuthenticationFilter.java:150` - SecurityContextHolder has no Quarkus equivalent. This reads/writes the Spring thread-local security context. On Quarkus, the identity should come from SecurityIdentity (injected) and API-key auth should be handled by a custom IdentityProvider rather than imperatively setting the context.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/filter/JwtAuthenticationFilter.java:176` - the previous ApiKeyAuthenticationToken extended Spring Security's AbstractAuthenticationToken. It is now a plain POJO that does not implement the security-compat Authentication contract, so it cannot be stored in the SecurityContext. Build a compat UsernamePasswordAuthenticationToken from the user's authorities to keep the API-key authentication intent; in Quarkus this should be a SecurityIdentity produced by a custom ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/filter/JwtAuthenticationFilter.java:220` - SecurityContextHolder/UsernamePasswordAuthenticationToken. Building a Spring authentication token and pushing it into the thread-local context must be replaced by producing a Quarkus SecurityIdentity (via IdentityProvider/ SecurityIdentityAugmentor) from the validated JWT claims. The user-loading logic (userDetailsService.loadUserByUsername) can be kept as a plain service call.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/filter/JwtAuthenticationFilter.java:243` - Spring's WebAuthenticationDetailsSource (remote address + session id) has no Quarkus equivalent. Storing the request as the details object keeps the call compile-safe; in Quarkus this metadata is available from the RoutingContext / SecurityIdentity.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/filter/ParticipantRateLimitInterceptor.java:71` - Do not trust X-Forwarded-For: it is user-controlled and trivially spoofed, which would allow an attacker to bypass this rate limiter by rotating fake IPs. Operators who deploy behind a trusted reverse proxy should configure Quarkus' quarkus.http.proxy.* (proxy-address-forwarding / trusted-proxies) at the framework level instead. ContainerRequestContext does not expose the remote address. Inject quarkus' RoutingContext ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/filter/UserAuthenticationFilter.java:40` - @Profile("!saas") had no direct annotation equivalent here. Gate this filter's activation on the "saas" build profile (e.g. via @io.quarkus.arc.profile.UnlessBuildProfile or a runtime check) and register it through Quarkus (quarkus-undertow @WebFilter or a jakarta.ws.rs.container.ContainerRequestFilter @Provider). Registration ordering relative to the other security filters (JwtAuthenticationFilter, *RateLimitingFilter) must be ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/filter/UserAuthenticationFilter.java:83` - Start each request clean so a pooled thread can't inherit a prior request's key label - but keep a label an upstream filter (JwtAuthenticationFilter) already set for a request it API-key-authenticated. ApiKeyAuthenticationToken is a plain POJO here, so "already authenticated upstream" is the closest available test.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/filter/UserAuthenticationFilter.java:90` - Spring's OncePerRequestFilter#shouldNotFilter behavior: skip the filter body for static resources, SPA routes and public API endpoints. ensure the Quarkus filter registration does not run this filter more than once per request (the OncePerRequestFilter guarantee).
-`app/proprietary/src/main/java/stirling/software/proprietary/security/filter/UserAuthenticationFilter.java:319` - Was Spring's OncePerRequestFilter#shouldNotFilter; now called explicitly at the top of doFilter. if registered as a ContainerRequestFilter instead of a servlet Filter, fold this skip logic into the request filter using UriInfo.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/filter/UserBasedRateLimitingFilter.java:32` - Servlet filter retained (quarkus-undertow). Spring's OncePerRequestFilter replaced by a plain jakarta.servlet.Filter registered as a CDI bean via @WebFilter so it covers all requests; the rate-limiting logic operates on the raw HttpServletRequest/HttpServletResponse which a JAX-RS ContainerRequestFilter does not expose as conveniently. Spring's @Profile("!saas") gated this filter so it was NOT registered in the "saas" profile ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/filter/UserBasedRateLimitingFilter.java:48` - SecurityContextHolder replaced by injected SecurityIdentity. SecurityIdentity is request-scoped and is populated by Quarkus security extensions (quarkus-elytron-security / quarkus-oidc / etc.) once authentication is migrated. Until then it will be anonymous and getRoleFromIdentity will fall through to the IllegalStateException.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/model/ApiKeyAuthenticationToken.java:6` - this class extended Spring Security's org.springframework.security.authentication.AbstractAuthenticationToken (which implements org.springframework.security.core.Authentication). Quarkus has no equivalent token type; the runtime principal model is io.quarkus.security.identity.SecurityIdentity, typically built via a custom IdentityProvider / SecurityIdentityAugmentor for the API-key auth path. This class has been reduced to a ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/TauriAuthorizationRequestResolver.java:6` - this class implemented Spring Security's org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver SPI, wrapping DefaultOAuth2AuthorizationRequestResolver (built from a ClientRegistrationRepository) to inject a custom "tauri:" state value before the authorization request is sent to the OAuth2 provider. quarkus-oidc has no equivalent pluggable AuthorizationRequestResolver SPI. The Spring glue ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/service/UserService.java:167` - Resolve through the shared service (multi-key table, then the legacy per-user column). The key runs as its owner with the owner's authorities. emits a Spring-shaped Authentication consumed by the auth filters; replace with a SecurityIdentity construction once the filter layer is ported.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/session/SessionPersistentRegistry.java:25` - this class implements the SessionRegistry compatibility shim (stirling.software.common.security.SessionRegistry) and exposes SessionInformation, UserDetails and OAuth2User from the same compat package. Quarkus has no equivalent session-registry abstraction. These shim types are kept ONLY because un-migrated collaborators (UserAuthenticationFilter, UserService, SessionRegistryConfig) still consume this interface and its return ...
-`app/proprietary/src/main/java/stirling/software/proprietary/web/AuditWebFilter.java:25` - Servlet filter retained (quarkus-undertow). Spring's OncePerRequestFilter replaced by a plain jakarta.servlet.Filter registered as a CDI bean via @WebFilter so it covers all requests. Spring's @Order(Ordered.HIGHEST_PRECEDENCE + 10) ordering has no direct @WebFilter equivalent; if this filter must run before other servlet filters, configure ordering explicitly (e.g. via a FilterRegistrationBean equivalent / quarkus.http.filter.* ...
-`app/proprietary/src/main/java/stirling/software/proprietary/web/CorrelationIdFilter.java:22` - quarkus-undertow provides jakarta.servlet support. Register this filter and its URL mapping/ordering via a @WebFilter annotation or a ServletExtension if order matters (Spring auto-registered @Component filters; Quarkus does not).
-`app/saas/build.gradle:14` - spring-boot-starter-webmvc -> quarkus-rest (inherited api-scoped from :common). REMOVED: spring-boot-starter-aspectj - no AspectJ in Quarkus; use quarkus-arc CDI interceptors. rewrite any @Aspect advice (e.g. CreditSuccessAdvice) as CDI interceptors.
-`app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:344` - @PreAuthorize("@teamSecurity.isTeamMember(#teamId)") complex SpEL; enforce team-membership check programmatically or via a JAX-RS filter.
-`app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:363` - @PreAuthorize("@teamSecurity.isTeamLeader(#teamId)") complex SpEL; enforce team-leader check programmatically or via a JAX-RS filter.
-`app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:382` - @PreAuthorize("@teamSecurity.isTeamLeader(#teamId)") complex SpEL; enforce team-leader check programmatically or via a JAX-RS filter.
-`app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:436` - @PreAuthorize("@teamSecurity.isTeamMember(#teamId)") complex SpEL; enforce team-membership check programmatically or via a JAX-RS filter.
-`app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:485` - @PreAuthorize("@teamSecurity.isTeamLeader(#teamId)") complex SpEL; enforce team-leader check programmatically or via a JAX-RS filter.
-`app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:727` - @PreAuthorize("@teamSecurity.isTeamMember(#teamId) or hasRole('ADMIN')") complex SpEL; enforce team-membership-or-admin check programmatically or via a JAX-RS filter.
-`app/saas/src/main/java/stirling/software/saas/controller/UserRoleWebhookController.java:195` - @PreAuthorize("isAuthenticated()") complex SpEL; enforce authenticated access via JAX-RS SecurityContext / filter. inject Principal via @jakarta.ws.rs.core.Context SecurityContext (JAX-RS does not bind a bare java.security.Principal parameter like Spring MVC).
-`app/saas/src/main/java/stirling/software/saas/payg/api/PaygWalletController.java:72` - cap is enforced application-side via the entitlement guard) and invalidates the team's snapshot cache. Only leaders may call this; the team is derived from the caller, so we authorise inside the method — the team id never appears on the path or query string. was a Spring {@code@RestController} with method-injected {@code Authentication} and {@code@PreAuthorize("isAuthenticated()")}. Now JAX-RS: auth comes from the {@link ...
-`app/saas/src/main/java/stirling/software/saas/payg/cap/AiToolRoutes.java:29` - literal value of the former Spring constant HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE. Replace with the JAX-RS route template (UriInfo / ResourceInfo) once the interceptor is converted to a @Provider filter.
-`app/saas/src/main/java/stirling/software/saas/payg/charge/JobInput.java:30` - Part/MultipartFile bridge. The ingress interceptor (PaygChargeInterceptor) is now servlet-native and constructs inputs from jakarta.servlet.http.Part rather than Spring's MultipartFile. The downstream classifier still consumes the stirling.software.common.model.MultipartFile abstraction (size + content-type + input stream). This constructor adapts a Part into that abstraction so both the untouched interceptor and the classifier ...
-`app/saas/src/main/java/stirling/software/saas/payg/entitlement/EntitlementGuard.java:64` - pipeline must never block a customer because the guard tripped on a transient DB error. was a Spring {@code@Component} implementing {@code HandlerInterceptor}. Convert to a JAX-RS {@code@Provider} ContainerRequestFilter (priority {@code PaygWebMvcConfig.ENTITLEMENT_GUARD_ORDER}). Handler-annotation introspection now uses a reflective {@link Method} fallback; HTTP status/header/media-type constants are inlined literals.
-`app/saas/src/main/java/stirling/software/saas/payg/filter/PaygChargeInterceptor.java:69` - and counted on {@code payg.filter.errors}. The customer's tool call always proceeds. was a Spring {@code@Component} ({@code@Profile("saas")}) implementing {@code AsyncHandlerInterceptor}. Convert to a JAX-RS {@code@Provider} request/response filter pair. Handler-annotation introspection now uses a reflective {@link Method} fallback (see {@link#resolveResourceMethod}); multipart access uses the servlet-native {@link Part} API ...
-`app/saas/src/main/java/stirling/software/saas/payg/filter/PaygChargeInterceptor.java:91` - literal value of the former Spring constant {@code HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE}. Replace with the JAX-RS route template obtained from {@code@Context UriInfo} / {@code ResourceInfo} during the filter conversion.
-`app/saas/src/main/java/stirling/software/saas/payg/filter/PaygChargeInterceptor.java:181` - was @Override AsyncHandlerInterceptor#preHandle(request, response, handler). Convert to a JAX-RS ContainerRequestFilter.
-`app/saas/src/main/java/stirling/software/saas/payg/filter/PaygChargeInterceptor.java:242` - was `request instanceof MultipartHttpServletRequest mreq` + mreq.getMultiFileMap(). Now uses servlet-native request.getParts(). A non-multipart request yields no file parts and short-circuits, preserving the original behavior.
-`app/saas/src/main/java/stirling/software/saas/payg/filter/PaygChargeInterceptor.java:332` - the {@link JobInput} record's first component is still Spring's {@code MultipartFile} (owned by another module). This interceptor now sources inputs from the servlet {@link Part} API. Once {@code JobInput} is migrated to carry a {@link Part} (or a neutral size+content-type holder), construct it directly here: {@code return new JobInput(part, path);}. Kept as a single adaptation seam so the rest of the charge flow is untouched.
-`app/saas/src/main/java/stirling/software/saas/payg/filter/PaygChargeInterceptor.java:343` - was @Override AsyncHandlerInterceptor#afterCompletion(request, response, handler, Exception). Convert to a JAX-RS ContainerResponseFilter.
-`app/saas/src/main/java/stirling/software/saas/payg/filter/PaygChargeInterceptor.java:488` - was @Override AsyncHandlerInterceptor#afterConcurrentHandlingStarted. JAX-RS handles async dispatch differently; no direct equivalent required.
-`app/saas/src/main/java/stirling/software/saas/payg/filter/PaygFilterProperties.java:24` - @ConfigurationProperties(prefix="payg.filter"); bind via @ConfigProperty or @ConfigMapping
-`app/saas/src/main/java/stirling/software/saas/payg/filter/PaygResponseBodyWrapperFilter.java:33` - this was a Spring {@code OncePerRequestFilter} ({@code@Component@Profile("saas")}). It must be re-registered as a {@code jakarta.servlet.Filter} (or a JAX-RS {@code@jakarta.ws.rs.ext.Provider} ContainerResponse filter pair) and ordered ahead of the PAYG interceptor so the response wrapper is available in afterCompletion. The Spring base class provided once-per-request dispatch and the {@code doFilterInternal} hook; that ...
-`app/saas/src/main/java/stirling/software/saas/payg/filter/PaygResponseBodyWrapperFilter.java:61` - was @Override of Spring OncePerRequestFilter#doFilterInternal. Retains the servlet signature; invoke from the filter registration's doFilter once converted.
-`app/saas/src/main/java/stirling/software/saas/payg/filter/PaygWebMvcConfig.java:13` - Holds the PAYG hot-path ordering constants. Under Spring MVC these registered {@link PaygChargeInterceptor} and the entitlement guard as ordered interceptors; under Quarkus the interceptor/guard are JAX-RS filters that self-order via {@code@Priority}. The order constants remain the single source of truth for that relative ordering. the Spring {@code WebMvcConfigurer#addInterceptors} registration was removed. Re-express it as ...
-`app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java:55` - Stateless JWT authentication filter for the saas profile. this was a Spring {@code OncePerRequestFilter}. It must be re-registered as a JAX-RS {@code@jakarta.ws.rs.container.ContainerRequestFilter} with {@code@jakarta.ws.rs.ext.Provider} (or a {@code jakarta.servlet.Filter}) and ordered before the Quarkus OIDC/auth processing. The {@code doFilterInternal}/{@code shouldNotFilter} servlet signatures are retained here; the ...
-`app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java:72` - placeholder for Spring's {@code org.springframework.security.oauth2.jwt.JwtDecoder}. Replace with Quarkus OIDC token parsing that yields a verified {@link JsonWebToken} (or throws on invalid token).
-`app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java:88` - the Spring AuthenticationEntryPoint (BearerTokenAuthenticationEntryPoint) that wrote the 401 challenge has no Quarkus equivalent here. When converting to a JAX-RS @Provider filter, emit the 401 / WWW-Authenticate response directly (or delegate to Quarkus OIDC) in place of authenticationEntryPoint.commence(...).
-`app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java:108` - this retains the original OncePerRequestFilter.doFilterInternal behavior. Wire it into a JAX-RS ContainerRequestFilter / servlet Filter. The error branch previously called authenticationEntryPoint.commence(request, response, e); emit the 401 response directly during that conversion.
-`app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java:143` - was authenticationEntryPoint.commence(request, response, e) (Spring BearerTokenAuthenticationEntryPoint). Emit the 401 challenge response here when converting to a JAX-RS @Provider filter.
-`app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java:219` - previously caught Spring's JwtException and rethrew InvalidBearerTokenException("Invalid JWT", e). Adjust to the exception type thrown by the Quarkus OIDC token parser.
-`app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java:313` - was Spring's DataIntegrityViolationException (email-collision race). jakarta.persistence.PersistenceException is broader; narrow to the Hibernate/JPA constraint-violation type once the persistence layer is finalized.
-`app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java:409` - Concurrent creation; fall through, the row exists. was Spring's DataIntegrityViolationException. Narrow to the Hibernate/JPA constraint-violation type once the persistence layer is finalized.
-`app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java:424` - Parallel filter won the race; fetch the winning row. was Spring's DataIntegrityViolationException. Narrow to the Hibernate/JPA constraint-violation type once the persistence layer is finalized.
-`app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java:457` - ApiKeyAuthenticationToken is a plain POJO that does not implement the Authentication shim. Wrap the principal/credentials/authorities in a UsernamePasswordAuthenticationToken (which does) so it can be set on the SecurityContext. Re-wire to a Quarkus SecurityIdentity when the API-key auth path is migrated.
-`app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java:483` - --------------------------------------------------------------------------------------------- claim accessor adapters. Spring's Jwt exposed typed claim getters (getClaimAsString/getClaimAsStringList/getClaimAsInstant/getClaimAsBoolean). MicroProfile JsonWebToken only exposes a generic getClaim(name); these helpers reproduce the original typed semantics so the validation/user-creation logic is preserved unchanged ...
-`app/saas/src/main/java/stirling/software/saas/security/SupabaseSecurityConfig.java:38` - Stateless Supabase-JWT security chain. this class was a Spring {@code@Configuration} with {@code@EnableWebSecurity}, {@code@EnableMethodSecurity}, {@code@Profile("saas")} and {@code@Order(1)}. The {@code SecurityFilterChain} bean (CSRF/CORS/session/oauth2ResourceServer wiring) has no Quarkus equivalent and must be re-expressed declaratively via {@code quarkus.http.auth.*} config plus Quarkus OIDC/SmallRye-JWT. The {@code ...
-`app/saas/src/main/java/stirling/software/saas/security/SupabaseSecurityConfig.java:72` - the original @Bean SecurityFilterChain saasSecurityFilterChain(...) configured CSRF-disabled, CORS, STATELESS sessions, permitAll matchers for OPTIONS/actuator-health/config/static/public-auth/frontend routes, anyRequest().authenticated(), registered SupabaseAuthenticationFilter before BearerTokenAuthenticationFilter, set a BearerTokenAuthenticationEntryPoint + BearerTokenAccessDeniedHandler, and wired ...
-`app/saas/src/main/java/stirling/software/saas/security/SupabaseSecurityConfig.java:168` - original @Bean CorsConfigurationSource configured CORS for the Spring SecurityFilterChain (allowed origins/methods/headers, exposed header WWW-Authenticate, allowCredentials=true, maxAge=3600). Re-express via quarkus.http.cors.* properties. The origin-resolution logic (operator override vs. defaults, the Tauri desktop origins, and the wildcard warning) is retained below as a helper for that translation.
-`app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesSaml2ResourceTest.java:15` - Spring Boot test framework not available in Quarkus
-`app/proprietary/build.gradle:44` - ---- SAML2: no native Quarkus extension. Rehosted on OpenSAML 5 (already pinned via openSamlVersion) following the dnulnets/quarkus-saml example. spring-security-saml2- service-provider and spring-security-core are removed; the SAML wiring is reimplemented on a Jakarta servlet + OpenSAML 5 (quarkus-undertow provides the servlet runtime). reimplement Saml2Configuration / CustomSaml2* on OpenSAML 5. ----
-`app/proprietary/src/main/java/stirling/software/proprietary/config/AsyncConfig.java:54` - this previously wrapped the executor in Spring Security's DelegatingSecurityContextExecutor to propagate the SecurityContext onto background threads. Quarkus has no direct equivalent; the SecurityIdentity must be captured on the caller thread and re-established on the worker thread (e.g. via a captured io.quarkus.security.identity.SecurityIdentity or org.eclipse.microprofile.context.ThreadContext from MicroProfile Context ...
-`app/proprietary/src/main/java/stirling/software/proprietary/controller/api/ProprietaryUIDataController.java:447` - Spring distinguished UserDetails / OAuth2User / CustomSaml2AuthenticatedPrincipal off authentication.getPrincipal() to set the oAuth2Login / saml2Login flags. Under Quarkus the auth mechanism is exposed via SecurityIdentity attributes (e.g. quarkus-oidc IdToken / SAML augmentor). Until OAuth2/ SAML are wired to quarkus-oidc, only the username is resolved and the login-type flags default to false.
-`app/proprietary/src/main/java/stirling/software/proprietary/controller/api/SignatureController.java:36` - Controller for managing user signatures in proprietary/authenticated mode only. Requires user authentication and enforces per-user storage limits. the original endpoints were guarded by Spring Security SpEL expressions ({@code@PreAuthorize("isAuthenticated() && !hasAuthority('ROLE_DEMO_USER')")} and {@code@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")}). These are not simple role checks, so they cannot be expressed with ...
-`app/proprietary/src/main/java/stirling/software/proprietary/mcp/McpServerController.java:229` - the Spring code derived scopes from GrantedAuthority values prefixed with "SCOPE_". Quarkus SecurityIdentity.getRoles() typically already carries the bare role/scope names (quarkus-oidc maps OIDC scopes to roles without the SCOPE_ prefix). Confirm the configured quarkus.oidc role/scope mapping; if scopes arrive as a "scope" claim, read them via securityIdentity.getAttribute("scope")/getClaims() instead. For now we accept both ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationFailureHandler.java:56` - replace Spring exception type checks below (DisabledException, LockedException, BadCredentialsException, UsernameNotFoundException, InternalAuthenticationServiceException) with the Quarkus authentication-failure type(s), and replace each getRedirectStrategy().sendRedirect(request, response, "...") call with a Quarkus redirect (e.g. response.sendRedirect(...) or building a 302 jakarta.ws.rs.core.Response from the auth mechanism).
-`app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationFailureHandler.java:102` - default failure handling previously delegated to SimpleUrlAuthenticationFailureHandler.onAuthenticationFailure (redirect to the configured failure URL).
-`app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationFailureHandler.java:107` - these predicates stand in for Spring Security's exception type hierarchy and must be rewired to the Quarkus authentication-failure type(s) once the auth mechanism is chosen.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationSuccessHandler.java:58` - signature changed from Spring's onAuthenticationSuccess(HttpServletRequest, HttpServletResponse, org.springframework.security.core.Authentication). The Spring Authentication parameter has been dropped here; JwtServiceInterface#generateToken(Authentication, ...) still requires it (JwtServiceInterface is a separate file that must be migrated to accept a Quarkus SecurityIdentity / principal). For now the username is read from the ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationSuccessHandler.java:78` - JwtServiceInterface#generateToken expected a Spring Authentication. Pass the migrated Quarkus identity once JwtServiceInterface is ported; generating the token by username for now.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationSuccessHandler.java:112` - placeholder for reading the redirect URL off whatever object the migrated request cache stores. The Spring SavedRequest#getRedirectUrl() is gone.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java:131` - the following Spring-Security collaborators were injected as @Autowired(required=false) optional beans and consumed only inside the removed HttpSecurity DSL (GrantedAuthoritiesMapper, RelyingPartyRegistrationRepository, OpenSaml5AuthenticationRequestResolver, ClientRegistrationRepository, PasswordEncoder). They are dropped here because their types are Spring-Security-only; reintroduce equivalents (quarkus-oidc client config ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AuthController.java:280` - was SecurityContextHolder.getContext().getAuthentication(). Quarkus SecurityIdentity has no Spring UserDetails principal; loading the full User here requires a SecurityIdentityAugmentor that attaches the User (or re-loading via userDetailsService by name). Until then we re-load the user from the identity name.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/identity/UserSecurityIdentityAugmentor.java:25` - Attaches the {@link User} entity as the {@link SecurityIdentity} principal for any authenticated request. Spring exposed the {@code User} directly via {@code Authentication#getPrincipal()} (it implemented {@code UserDetails}), so a lot of the code base does {@code principal instanceof User} (folders, file storage, sessions, audit, UserController). This augmentor restores that for the Quarkus auth paths (JWT Bearer, X-API-KEY, and later OIDC/SAML): it ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/model/Authority.java:20` - this entity previously implemented Spring Security's org.springframework.security.core.GrantedAuthority. That interface only required String getAuthority(), which the Lombok @Getter on the 'authority' field still provides. Quarkus uses its own role model (SecurityIdentity roles); when wiring the IdentityProvider that loads users, map this 'authority' value into the granted roles.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/model/User.java:40` - this entity previously implemented org.springframework.security.core.userdetails.UserDetails. Quarkus has no UserDetails contract; the user-loading/principal adaptation must be rehosted in a Quarkus IdentityProvider (or SecurityIdentityAugmentor) that builds a SecurityIdentity from this entity. The Lombok getters still expose getUsername()/getPassword()/getAuthorities()/ isEnabled() so that adapter can read them directly ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/model/exception/AuthenticationFailureException.java:3` - originally extended org.springframework.security.core.AuthenticationException (Spring Security). Quarkus has no direct equivalent base type; extend RuntimeException so this remains a usable application exception. If integrated with quarkus-security, consider mapping to io.quarkus.security.AuthenticationFailedException.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:33` - this class extended Spring Security's SavedRequestAwareAuthenticationSuccessHandler, which has no Quarkus equivalent. Under quarkus-oidc there is no AuthenticationSuccessHandler concept; the post-login OAuth2 success flow must be rehosted, e.g. via a SecurityIdentityAugmentor plus a JAX-RS callback resource (or a jakarta.servlet endpoint) that performs the redirect/JWT-issuance below. The Spring ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:54` - the original signature took a Spring Security org.springframework.security.core.Authentication. Under quarkus-oidc this should receive an io.quarkus.security.identity.SecurityIdentity (or the OIDC IdToken/UserInfo). The "authentication" parameter is now typed as Object so the body still compiles; replace it with the real quarkus-oidc principal type and re-implement principal extraction below when wiring the success flow.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:67` - principal extraction relied on Spring Security OAuth2User / UserDetails. Derive the username from the quarkus-oidc principal (SecurityIdentity / IdToken claims) instead.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:100` - SavedRequest / "SPRING_SECURITY_SAVED_REQUEST" is a Spring Security web construct. Under quarkus-oidc the original target URL is preserved via the OIDC state/restore-path mechanism (quarkus.oidc.authentication.restore-path-after-redirect) rather than a session attribute. Re-implement saved-request resolution accordingly; the session attribute read below is left as a placeholder and will currently be null.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:112` - originally delegated to SavedRequestAwareAuthenticationSuccessHandler.onAuthenticationSuccess to redirect to the saved request. Reimplement the redirect to the saved/original destination here once the quarkus-oidc saved-request mechanism is in place.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:122` - originally threw Spring Security's org.springframework.security.authentication.LockedException. Replace with the exception type the quarkus-oidc success flow expects (or a redirect to a locked page); throwing a plain IllegalStateException here as a placeholder.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:130` - originally used Spring's RedirectStrategy via getRedirectStrategy().sendRedirect(...). Using the servlet response directly.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:154` - SSO provider/claims extraction relied on Spring Security's OAuth2User attributes and OAuth2AuthenticationToken. Re-derive the OIDC "sub" claim and the provider registration id from the quarkus-oidc principal.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:189` - Web: Use default expiry JwtServiceInterface.generateToken(Authentication, claims) takes a Spring Security Authentication. Until JwtServiceInterface is migrated, issue the token by username (same identity) to avoid the Spring dependency here.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:214` - placeholder for principal -> username extraction. Originally used Spring Security OAuth2User.getName() / UserDetails.getUsername(). Implement against the quarkus-oidc principal (SecurityIdentity.getPrincipal().getName() / IdToken claims).
-`app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:219` - "extract username from the quarkus-oidc principal");
-`app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:222` - placeholder for the OIDC "sub" claim. Originally oAuth2User.getAttribute("sub"). Read it from the quarkus-oidc IdToken/UserInfo.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:226` - "extract the 'sub' claim from the quarkus-oidc principal");
-`app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:229` - placeholder for the saved-request redirect URL. Originally SavedRequest.getRedirectUrl().
-`app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:233` - "resolve the saved-request redirect URL under quarkus-oidc");
-`app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:236` - placeholder for delegating to the saved-request redirect. Originally SavedRequestAwareAuthenticationSuccessHandler.onAuthenticationSuccess(...).
-`app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:242` - "redirect to the saved/original destination under quarkus-oidc");
-`app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:252` - originally cast to Spring Security's OAuth2AuthenticationToken and called getAuthorizedClientRegistrationId(). Derive the OIDC provider/tenant id from the quarkus-oidc principal instead.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:388` - originally built the Set-Cookie value with Spring's org.springframework.http.ResponseCookie. Replaced with a manually built RFC 6265 Set-Cookie string to drop the Spring HTTP dependency. Consider switching to jakarta.servlet.http.Cookie / response.addCookie once SameSite handling is confirmed.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/OAuth2Configuration.java:27` - OAuth2 client/login is a Spring Security feature (org.springframework.security.oauth2.client.*) with NO direct Quarkus equivalent. In Quarkus the OIDC/OAuth2 client is configured declaratively via quarkus-oidc (quarkus.oidc.* and named tenants quarkus.oidc.<tenant>.* in application.properties), not by programmatically building a ClientRegistrationRepository. This class previously @Produces'd a ClientRegistrationRepository and a ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/OAuth2Configuration.java:50` - @Lazy has no Quarkus equivalent; CDI proxies break the original lazy cycle. UserService is injected eagerly. If a genuine lazy/circular dependency exists, switch to jakarta.enterprise.inject.Instance<UserService> and resolve at call time.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/OAuth2Configuration.java:70` - Resolves the set of configured OAuth2 providers from ApplicationProperties and validates each one. The original implementation built a Spring Security ClientRegistrationRepository from these providers. the return type was org.springframework.security.oauth2.client.registration.ClientRegistrationRepository, produced via Spring @Bean. quarkus-oidc does not consume a ClientRegistrationRepository; instead each validated Provider ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/OAuth2Configuration.java:116` - the original built a ClientRegistration via ClientRegistrations.fromIssuerLocation(issuer) (OIDC discovery). Under quarkus-oidc this maps to quarkus.oidc.<name>.auth-server-url=<issuer> with discovery enabled, plus client-id/credentials.secret/authentication.scopes/token-state username attribute.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/OAuth2Configuration.java:139` - the original built a ClientRegistration with explicit authorizationUri/tokenUri/userInfoUri + redirectUri(REDIRECT_URI_PATH + name) + AUTHORIZATION_CODE grant. Under quarkus-oidc this maps to a named tenant quarkus.oidc.google.* (authorization-path/token-path/user-info-path or auth-server-url, authentication.redirect-path, application-type=web-app). Google's endpoints come from the GoogleProvider getters below.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/OAuth2Configuration.java:175` - the original built a ClientRegistration with explicit authorizationUri/tokenUri/userInfoUri + redirectUri(REDIRECT_URI_PATH + name) + AUTHORIZATION_CODE grant. Map to quarkus.oidc.github.* tenant config (GitHub is a plain OAuth2, not OIDC, provider - quarkus-oidc may require provider=github or explicit *-path settings).
-`app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/OAuth2Configuration.java:220` - the original built a ClientRegistration via ClientRegistrations.fromIssuerLocation(issuer) (OIDC discovery) with redirectUri(REDIRECT_URI_PATH + name) + AUTHORIZATION_CODE grant. Map to a named tenant quarkus.oidc.<name>.auth-server-url=<issuer> (discovery on), client-id/credentials.secret/authentication.scopes, authentication.redirect-path.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/OAuth2Configuration.java:241` - this was a Spring Security
-`app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CertificateUtils.java:19` - the original @ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true") gated this class on a runtime property. This is a utility holding only static methods (not a CDI bean), so the annotation was a no-op for instantiation and is dropped. Callers must enforce the security.saml2.enabled runtime toggle (e.g. via a runtime guard at the SAML SP entry point); see the SAML2 migration notes.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2AuthenticatedPrincipal.java:7` - this record implemented Spring Security's org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal. There is NO Quarkus SAML extension; the SAML SP must be rehosted on a Jakarta @WebServlet using OpenSAML 5 (dnulnets/quarkus-saml pattern). The OpenSAML-derived principal data (name, attributes, nameId, sessionIndexes) is preserved below as a plain data carrier; re-wire it into the replacement ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2ResponseAuthenticationConverter.java:23` - there is NO Quarkus SAML extension. This class previously implemented Spring Security's org.springframework.security.core.convert.converter.Converter< OpenSaml5AuthenticationProvider.ResponseToken, Saml2Authentication> to plug into Spring's SAML2 OpenSaml5AuthenticationProvider pipeline. The OpenSAML 5 (org.opensaml.*) assertion/attribute extraction logic below is preserved unchanged. The Spring SAML2 glue has been removed: - ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2ResponseAuthenticationConverter.java:83` - signature changed from convert(OpenSaml5AuthenticationProvider.ResponseToken) returning Saml2Authentication. Re-wire the input to the OpenSAML 5 Assertion obtained from the rehosted SAML SP and the output to a Quarkus SecurityIdentity. The OpenSAML attribute/identifier/session-index extraction logic below is the reusable part and is preserved. The returned CustomSaml2AuthenticatedPrincipal plus the resolved role (ROLE_USER or ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2ResponseAuthenticationConverter.java:115` - resolved authority was previously wrapped in a Spring SimpleGrantedAuthority("ROLE_USER" / userService.findRole(user)). Map this role String onto a Quarkus SecurityIdentity role when wiring the SAML SP / SecurityIdentityAugmentor.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/JwtSaml2AuthenticationRequestRepository.java:13` - this class implemented Spring Security's org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository over Saml2PostAuthenticationRequest / RelyingPartyRegistration(Repository). There is NO Quarkus SAML extension, so the Spring Security SAML glue (interface, Saml2PostAuthenticationRequest, RelyingPartyRegistration[Repository]) has been removed. The SAML SP must be rehosted on a Jakarta @WebServlet ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/JwtSaml2AuthenticationRequestRepository.java:39` - original signature was saveAuthenticationRequest(Saml2PostAuthenticationRequest authRequest, HttpServletRequest, HttpServletResponse). Pass the OpenSAML-derived claims + relayState once the SP is rehosted.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/JwtSaml2AuthenticationRequestRepository.java:66` - original returned Saml2PostAuthenticationRequest. Map the returned claims back to the OpenSAML AuthnRequest model once the SP is rehosted.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/JwtSaml2AuthenticationRequestRepository.java:80` - original returned Saml2PostAuthenticationRequest.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/JwtSaml2AuthenticationRequestRepository.java:112` - original signature was serializeSamlRequest(Saml2PostAuthenticationRequest authRequest). Build this claims map from the OpenSAML AuthnRequest fields (id, relyingPartyRegistrationId / SP entity id, authenticationRequestUri / destination, samlRequest, relayState) once the SP is rehosted.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/JwtSaml2AuthenticationRequestRepository.java:133` - original returned Saml2PostAuthenticationRequest rebuilt via Saml2PostAuthenticationRequest.withRelyingPartyRegistration(...). Resolve the RelyingPartyRegistration equivalent (SP metadata) and rebuild the OpenSAML AuthnRequest from these claims once the SP is rehosted. For now the raw claims map is returned unchanged.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/Saml2Configuration.java:19` - there is NO Quarkus SAML extension. The original class was a Spring @Configuration that exposed two @Bean factory methods producing Spring Security SAML2 types (org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository and ...web.authentication.OpenSaml5AuthenticationRequestResolver). Those builder/glue types have no Quarkus equivalent, so the Spring Security SAML2 imports and ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/Saml2Configuration.java:41` - originally a @Bean returning Spring Security's RelyingPartyRegistrationRepository built via RelyingPartyRegistration.withRegistrationId(...) (InMemoryRelyingPartyRegistrationRepository, Saml2X509Credential, Saml2MessageBinding). Those Spring Security SAML2 builder types are unavailable in Quarkus. The credential loading (CertificateUtils via the common Resource shim) and the entityId / ACS / SLO location strings are kept ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/Saml2Configuration.java:75` - was Saml2X509Credential.verification(idpCert). Re-create the IdP verification credential from idpCert using OpenSAML 5 (BasicX509Credential).
-`app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/Saml2Configuration.java:98` - was new Saml2X509Credential(privateKey, cert, Saml2X509CredentialType.SIGNING). Build the SP signing credential from the key/cert below using OpenSAML 5 (BasicX509Credential) instead of Spring Security's Saml2X509Credential.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/Saml2Configuration.java:125` - the following Spring Security RelyingPartyRegistration was built here and stored in an InMemoryRelyingPartyRegistrationRepository. Re-implement against the OpenSAML-5-based SP using entityId / acsLocation / sloResponseLocation, the IdP issuer (samlConf.getIdpIssuer()), SSO/SLO bindings (POST) and locations (samlConf.getIdpSingleLoginUrl() / samlConf.getIdpSingleLogoutUrl()), authnRequestsSigned and wantAuthnRequestsSigned both ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/Saml2Configuration.java:142` - originally a @Bean returning Spring Security's OpenSaml5AuthenticationRequestResolver, configured with a RelayState resolver and an AuthnRequest customizer. That resolver type is Spring-Security-specific and has no Quarkus equivalent. The RelayState logic (Tauri detection -> TauriSamlUtils.buildRelayState(nonce)) and the AuthnRequest customization (unique ARQ id + logging) are PRESERVED below as helper methods so the ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/service/AppUpdateAuthService.java:24` - SecurityIdentity is request-scoped; injecting it into an @ApplicationScoped bean relies on Quarkus' client proxy resolving the current request's identity. Verify this resolves correctly when invoked outside an active HTTP request (e.g. scheduled/background contexts), where the identity may be anonymous/null.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/service/CustomOAuth2UserService.java:18` - quarkus-oidc has no equivalent of Spring's OAuth2UserService<OidcUserRequest, OidcUser> / OidcUserService delegate. Under quarkus-oidc the OIDC flow is handled by the extension (quarkus.oidc.* config); per-login user mapping and the "useAsUsername" claim selection should be re-implemented in a io.quarkus.security.identity.SecurityIdentityAugmentor (inject the @io.quarkus.oidc.IdToken JsonWebToken / OidcSession), and the ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/service/CustomOAuth2UserService.java:58` - Resolves and validates the local user for an OIDC login. this method previously implemented Spring's {@code OAuth2UserService<OidcUserRequest, OidcUser>.loadUser}. Under quarkus-oidc there is no user-request object handed to application code; instead call this logic from a {@code SecurityIdentityAugmentor} once quarkus-oidc has produced the {@code SecurityIdentity}. Provide the registration/tenant id, the merged claim map and ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/service/CustomOAuth2UserService.java:114` - was org.springframework.security.authentication .LockedException; surface this as io.quarkus.security.AuthenticationFailedException (or a custom locked-account exception) from the SecurityIdentityAugmentor.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/service/CustomOAuth2UserService.java:147` - was wrapped as org.springframework.security.oauth2.core.OAuth2AuthenticationException(OAuth2Error); rethrow as io.quarkus.security.AuthenticationFailedException from the augmentor.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/service/CustomOAuth2UserService.java:163` - was OAuth2AuthenticationException("Unexpected error during authentication"); rethrow as io.quarkus.security.AuthenticationFailedException.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/service/CustomUserDetailsService.java:16` - this class implemented org.springframework.security.core.userdetails.UserDetailsService and returned a org.springframework.security.core.userdetails.UserDetails. Quarkus has no UserDetailsService contract; the user-loading logic below should be invoked from a Quarkus IdentityProvider (or SecurityIdentityAugmentor) that turns the returned User into a SecurityIdentity. The method is retained as a plain service returning the User ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/service/JwtServiceInterface.java:17` - the implementation must derive the username/claims from SecurityIdentity (getPrincipal()/getRoles()) instead of the former Spring Authentication.getName()/getAuthorities().
-`app/proprietary/src/main/java/stirling/software/proprietary/security/service/UserService.java:684` - SessionPersistentRegistry still exposes Spring Security types (SessionInformation, UserDetails, OAuth2User). Once that collaborator is ported to a Quarkus session store, drop these Spring Security imports and adjust the principal type checks accordingly.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/session/SessionRegistryConfig.java:8` - SessionRegistryImpl is a Spring Security type (org.springframework.security.core.session.SessionRegistryImpl) with no Quarkus equivalent. Concurrent-session tracking must be rehosted (e.g. a custom bean backed by SessionPersistentRegistry / SecurityIdentity, or quarkus session management). The original producer was: @Bean public SessionRegistryImpl sessionRegistry() { return new SessionRegistryImpl(); }
-`app/proprietary/src/main/java/stirling/software/proprietary/security/supabase/SupabaseJwtDecoderFactory.java:12` - Produces the JWKS configuration for the proprietary Supabase login path. Only relevant when {@code security.supabase.user-login.enabled=true}. this class previously produced a Spring Security {@code org.springframework.security.oauth2.jwt.JwtDecoder} bean (Nimbus-based) via {@code@Configuration}/{@code@Bean}, conditionally registered with {@code@ConditionalOnProperty(security.supabase.user-login.enabled=true)}. Quarkus has no ...
-`app/proprietary/src/main/java/stirling/software/proprietary/service/AuditService.java:999` - Quarkus migration: was SecurityContextHolder.getContext().getAuthentication(). the original code distinguished API-key auth from web/JWT auth via `instanceof ApiKeyAuthenticationToken`. Under Quarkus the runtime principal is io.quarkus.security.identity.SecurityIdentity and ApiKeyAuthenticationToken has been reduced to a plain POJO (it is no longer the identity type), so the API-key vs WEB distinction can no longer be made by ...
-`app/proprietary/src/main/java/stirling/software/proprietary/storage/controller/FileStorageController.java:74` - SecurityIdentity replaces Spring's Authentication. The collaborator FileStorageService still exposes canAccessShareLink(FileShare, org.springframework.security .core.Authentication) and recordShareAccess(FileShare, Authentication, boolean). Once that service is migrated those methods should accept SecurityIdentity (or io.quarkus.security SecurityContext) and this injected identity can be passed through directly.
-`app/proprietary/src/main/java/stirling/software/proprietary/storage/controller/FileStorageController.java:271` - canAccessShareLink/recordShareAccess still take Spring Authentication. Passing null preserves the anonymous-deny behavior until the service is migrated to SecurityIdentity; once migrated, pass `securityIdentity` through instead.
-`app/proprietary/src/main/java/stirling/software/proprietary/storage/controller/FileStorageController.java:296` - canAccessShareLink still takes Spring Authentication; pass `securityIdentity` once FileStorageService is migrated.
-`app/proprietary/src/main/java/stirling/software/proprietary/storage/controller/FileStorageController.java:380` - Spring's Authentication-based anonymous check is replaced by SecurityIdentity. Verify "anonymous" semantics match once the security layer is migrated.
-`app/proprietary/src/main/java/stirling/software/proprietary/storage/service/FolderService.java:435` - a Quarkus SecurityIdentityAugmentor/IdentityProvider must attach the stirling.software.proprietary.security.model.User entity as the SecurityIdentity principal (Spring exposed it directly via Authentication#getPrincipal, since User used to implement UserDetails). Until that augmentor exists, this only resolves when the principal IS the User entity; otherwise it rejects as 401 rather than guessing at a username->User lookup.
-`app/proprietary/src/test/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandlerTest.java:26` - Spring Boot test framework not available in Quarkus
-`app/proprietary/src/test/java/stirling/software/proprietary/security/oauth2/TauriAuthorizationRequestResolverTest.java:15` - Spring Boot test framework not available in Quarkus
-`app/proprietary/src/test/java/stirling/software/proprietary/security/service/CustomOAuth2UserServiceDebugLoggingTest.java:46` - Spring Boot test framework not available in Quarkus
-`app/saas/src/main/java/stirling/software/saas/security/EnhancedJwtAuthenticationToken.java:15` - JWT auth token that exposes the Supabase subject UUID and email alongside the standard claims, so downstream code (audit, credit accounting) can avoid re-parsing the JWT every request. originally extended {@code org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken}. That Spring type has no Quarkus equivalent; it now extends the {@link AbstractAuthenticationToken} common shim and carries the ...
-`app/saas/src/main/java/stirling/software/saas/security/SupabaseSecurityConfig.java:84` - original @Bean JwtDecoder jwtDecoder() built a NimbusJwtDecoder from the Supabase JWKS endpoint (issuer + "/.well-known/jwks.json") and attached a SupabaseTokenValidator (iss/exp/aud enforcement with clock skew), failing closed when the issuer was unusable. NimbusJwtDecoder / JwtDecoder are Spring OAuth2 types with no Quarkus equivalent; configure Quarkus OIDC (quarkus.oidc.auth-server-url / mp.jwt.verify.* ) to point at the ...
-`app/saas/src/main/java/stirling/software/saas/security/SupabaseSecurityConfig.java:119` - Validates iss, exp (with clock-skew) and optionally aud on a decoded Supabase JWT. originally implemented Spring's {@code OAuth2TokenValidator<Jwt>} and returned {@code OAuth2TokenValidatorResult}. Those Spring OAuth2 types are gone; the validation now operates on {@link JsonWebToken} and returns the list of error messages (empty == valid). Re-wire this into Quarkus OIDC token validation.
-`app/saas/src/main/java/stirling/software/saas/util/AuthenticationUtils.java:95` - JsonWebToken principal from the Quarkus OIDC/JWT resource server was Spring's org.springframework.security.oauth2.jwt.Jwt; getClaimAsString("email") replaced with MicroProfile JsonWebToken.getClaim("email").
-`app/common/src/main/java/stirling/software/common/cluster/inprocess/InProcessClusterConfiguration.java:22` - the original @ConditionalOnExpression ("!${cluster.enabled:false} || '${cluster.backplane:inprocess}'.equalsIgnoreCase('inprocess')") gated activation of this whole configuration on a SpEL expression over two config properties. Quarkus/CDI has no direct equivalent for conditionally registering a producer set based on a SpEL boolean. The @DefaultBean producers below now always provide the in-process implementations unless another ...
-`app/common/src/main/java/stirling/software/common/cluster/inprocess/LocalDiskFileStoreConfiguration.java:17` - the original class was guarded by Spring's @ConditionalOnProperty(prefix="cluster", name="artifactStore", havingValue="local", matchIfMissing=true). Quarkus has no runtime equivalent: @io.quarkus.arc.profile.IfBuildProperty is build-time only and does not support matchIfMissing semantics. The producer below is now unconditional. The "local is the default; S3 supplies its own bean" behavior is preserved via @DefaultBean (the S3 ...
-`app/common/src/main/java/stirling/software/common/configuration/AppConfig.java:45` - <ul> <li>{@code@Bean} -> {@code@Produces}; {@code@Bean(name="x")} -> {@code@Produces@Named("x")}. <li>{@code@Value} -> {@code@ConfigProperty}; Spring {@code Environment} -> MicroProfile {@code Config}. <li>{@code@Profile("default")} flavor-default beans -> {@code@DefaultBean}: the :proprietary / :saas modules provide the "real" producer and automatically win when present, exactly like the old profile override (this is the Quarkus idiom for ...
-`app/core/src/main/java/stirling/software/SPDF/service/pdfjson/JobOwnershipServiceImpl.java:25` - MIGRATION: Spring's @ConditionalOnProperty(name="security.enable-login", havingValue="true") gated this bean. It is now @IfBuildProperty(security.enable-login=true) - the exact build-time complement of NoOpJobOwnershipService (@IfBuildProperty security.enable-login=false, enableIfMissing=true). The two are mutually exclusive at build time, so exactly one JobOwnershipService bean exists and callers can inject it directly (no Instance<> needed). A previous ...
-`app/core/src/main/java/stirling/software/SPDF/service/pdfjson/NoOpJobOwnershipService.java:17` - Spring's @ConditionalOnProperty(matchIfMissing=true) is a runtime condition; Quarkus @IfBuildProperty is evaluated at build time. enableIfMissing=true preserves the matchIfMissing default. If security.enable-login must be toggled at runtime, switch to @io.quarkus.arc.lookup.LookupIfProperty with Instance<JobOwnershipService> injection at use sites.
-`app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java:49` - the original class was guarded by Spring's @ConditionalOnProperty(prefix="telegram", name="enabled", havingValue="true"). Migrated to a runtime guard: the bean is always created, but register() (the @PostConstruct startup hook) short-circuits when the bot token/username are not configured, so an unconfigured Telegram integration stays inert. This is a true runtime toggle (no build-time pinning required).
-`app/proprietary/src/main/java/stirling/software/proprietary/cluster/ClusterMetrics.java:22` - original @ConditionalOnProperty(name = "cluster.enabled", havingValue = "true") was a runtime toggle. Quarkus @IfBuildProfile/@LookupIfProperty are build-time only. Either gate registration with a runtime guard on applicationProperties.getCluster().isEnabled() (e.g. skip meter registration when disabled), or use @io.quarkus.arc.lookup.LookupIfProperty if a build-time switch is acceptable.
-`app/proprietary/src/main/java/stirling/software/proprietary/cluster/ClusterNodeBootstrap.java:46` - Spring @ConditionalOnProperty(name = "cluster.enabled", havingValue = "true") was a runtime toggle. Quarkus build-time conditionals (@IfBuildProfile / @LookupIfProperty) cannot gate a StartupEvent observer at runtime, so the bean is always instantiated and the toggle is enforced at runtime via clusterEnabled below.
-`app/proprietary/src/main/java/stirling/software/proprietary/cluster/s3/S3FileStoreConfiguration.java:18` - the original Spring class was guarded by @ConditionalOnProperty(prefix="cluster", name="artifactStore", havingValue="s3") and @ConditionalOnMissingBean on the @Bean. The S3 producer below is gated with @io.quarkus.arc.lookup.LookupIfProperty(name="cluster.artifactStore", stringValue="s3"), which only contributes this FileStore when the property is "s3"; the always-on @DefaultBean producer in common's ...
-`app/proprietary/src/main/java/stirling/software/proprietary/cluster/valkey/ConditionalOnValkeyBackplane.java:23` - {@code@ConditionalOnExpression("${cluster.enabled:false} and '${cluster.backplane:inprocess}'.equals('valkey')")} SpEL guard. Quarkus/CDI has no SpEL-based conditional, but the boolean AND of two simple property checks maps directly onto two stacked (repeatable) {@link LookupIfProperty} annotations, which are evaluated with AND semantics. The Valkey producer beans are looked up only when both properties hold; otherwise the {@code@DefaultBean} in-process ...
-`app/proprietary/src/main/java/stirling/software/proprietary/cluster/valkey/ValkeyConnectionConfiguration.java:24` - this class was built on spring-data-redis types (LettuceConnectionFactory, StringRedisTemplate, RedisStandaloneConfiguration, LettuceClientConfiguration, RedisPassword, RedisConnection) plus direct io.lettuce.core usage. Quarkus has no spring-data-redis; the backplane should be reworked onto io.quarkus.redis.datasource.RedisDataSource / ReactiveRedisDataSource configured via quarkus.redis.* in application.properties (hosts ...
-`app/proprietary/src/main/java/stirling/software/proprietary/cluster/valkey/ValkeyJobStore.java:41` - @ConditionalOnValkeyBackplane (Spring @ConditionalOnExpression) is a runtime toggle on cluster.enabled + cluster.backplane=valkey. Quarkus has no direct equivalent for the composite expression; either reimplement ConditionalOnValkeyBackplane as a Quarkus build-time condition (@io.quarkus.arc.profile.IfBuildProfile / @io.quarkus.arc.lookup.LookupIfProperty) or guard bean activation at runtime. Annotation left in place pending ...
-`app/proprietary/src/main/java/stirling/software/proprietary/cluster/valkey/ValkeyKeyValueCache.java:18` - @ConditionalOnValkeyBackplane (a Spring @ConditionalOnExpression composite on cluster.enabled + cluster.backplane=valkey) has no direct CDI equivalent. Once that collaborator annotation is migrated, re-guard this bean (e.g. @io.quarkus.arc.lookup.LookupIfProperty or @io.quarkus.arc.profile.IfBuildProfile, or a runtime guard) so Valkey beans only load when cluster.enabled=true AND cluster.backplane=valkey. Build-time gating ...
-`app/proprietary/src/main/java/stirling/software/proprietary/mcp/McpServerController.java:36` - @ConditionalOnProperty(name = "mcp.enabled", havingValue = "true") -> LookupIfProperty. LookupIfProperty gates programmatic lookup; for a JAX-RS resource Quarkus always registers the endpoint. to truly disable the /mcp route when mcp.enabled=false, add a runtime guard (e.g. reject in handle() when disabled) or use a build-time conditional; LookupIfProperty alone does not unregister the REST path.
-`app/proprietary/src/main/java/stirling/software/proprietary/mcp/catalog/McpToolCatalog.java:34` - the original @ConditionalOnProperty(name = "mcp.enabled", havingValue = "true") gated this bean on a runtime property. Quarkus build-time conditions (@io.quarkus.arc.lookup.LookupIfProperty / @io.quarkus.arc.profile.IfBuildProfile) cannot honour a purely runtime toggle. The bean is now always present; callers must guard on applicationProperties.getMcp() / a runtime "mcp.enabled" check, or wire @LookupIfProperty on the injection ...
-`app/proprietary/src/main/java/stirling/software/proprietary/mcp/engine/EngineCapabilityClient.java:39` - @ConditionalOnProperty(name = "mcp.enabled", havingValue = "true") has no direct CDI equivalent. The onReady() observer below guards on a runtime config toggle instead; consider @io.quarkus.arc.lookup.LookupIfProperty / a build-time profile if the bean itself should be excluded.
-`app/proprietary/src/main/java/stirling/software/proprietary/mcp/tools/McpOperationExecutor.java:38` - the Spring @ConditionalOnProperty(name = "mcp.enabled", havingValue = "true") guard is not directly portable. For a build-time toggle use @io.quarkus.arc.lookup.LookupIfProperty(name = "mcp.enabled", stringValue = "true") on the injection points, or gate the call sites at runtime; this bean is otherwise always created.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/RateLimitResetScheduler.java:12` - Spring @Profile("!saas") gated this scheduler so it never ran in the "saas" profile. @io.quarkus.arc.profile.UnlessBuildProfile("saas") reproduces this when "saas" is a Quarkus BUILD profile; if "saas" is only a runtime profile, this annotation has no effect and the body of resetRateLimit() must instead short-circuit on a runtime profile check (org.eclipse.microprofile.config Config "quarkus.profile" / ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/DatabaseConfig.java:50` - MIGRATION NOTES (Spring -> Quarkus CDI): <ul> <li>{@code@Configuration} -> {@code@ApplicationScoped}; {@code@Bean} -> {@code@Produces}. <li>{@code@Qualifier("runningProOrHigher")} ctor param -> {@code@Inject} ctor with {@code@Named(...)} on the parameter (the producer lives in common {@code AppConfig}). <li>{@code@Profile("!saas")} on the producer -> {@code@UnlessBuildProfile("saas")} so the SaaS Postgres datasource shadows this H2 default ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/ee/EEAppConfig.java:31` - <ul> <li>{@code@Configuration} -> {@code@ApplicationScoped}; {@code@Bean(name="x")} -> {@code@Produces@Named("x")}. These producers deliberately omit {@code@DefaultBean} so they OVERRIDE the {@code@DefaultBean} producers declared in {@code stirling.software.common.configuration.AppConfig} whenever the :proprietary module is on the classpath - this is the Quarkus idiom for Spring's profile-based bean override. <li>{@code@Profile("security & ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/EmailController.java:31` - Spring @ConditionalOnProperty(mail.enabled) gated bean creation. CDI has no direct runtime-toggle equivalent; this controller is always registered and instead guards at request time via the injected mail.enabled config below. If the endpoint must be fully absent when mail is disabled, wire this with @io.quarkus.arc.lookup.LookupIfProperty or a build-time @io.quarkus.arc.profile.IfBuildProfile once a build/runtime decision is ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/model/api/Email.java:11` - dropped @ConditionalOnProperty("mail.enabled"). This is a request DTO, not a CDI bean, so conditional bean registration does not apply. The mail.enabled gate must be enforced on the consuming endpoint/service (e.g. via @IfBuildProfile / LookupIfProperty or a runtime guard on the email controller), not on this model.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/service/EmailService.java:21` - the original class was guarded by @ConditionalOnProperty(value = "mail.enabled", havingValue = "true", matchIfMissing = false). Quarkus has no @ConditionalOnProperty. mail.enabled is a runtime property (ApplicationProperties.Mail#isEnabled) rather than a build-time flag, so the bean is always produced and callers must guard on applicationProperties.getMail().isEnabled() at call time. SMTP connection settings now live under ...
-`app/saas/src/main/java/stirling/software/saas/security/TeamSecurityExpressions.java:27` - @Profile("saas") had no Quarkus equivalent here; gate bean availability via build profile / @IfBuildProfile if saas-only activation is required.
</details>
<details><summary><b>Spring Data -> Panache</b> (5)</summary>
-`app/core/src/main/java/stirling/software/SPDF/exception/GlobalExceptionHandler.java:65` - jakarta.ws.rs.ext.ExceptionMapper}. Because JAX-RS resolves at most one mapper per exception type, this single {@code ExceptionMapper<Throwable>} reproduces the original per-type {@code@ExceptionHandler} dispatch by inspecting the thrown exception with {@code instanceof}. The RFC 7807 body, previously a Spring {@code ProblemDetail}, is now built as an ordered {@link java.util.Map} (serialized by quarkus-rest-jackson) to preserve the exact response shape ...
-`app/proprietary/src/main/java/stirling/software/proprietary/repository/PersistentAuditEventRepository.java:24` - are preserved verbatim and executed through Panache's {@link#find(String, Object...)} / {@link#find(String, io.quarkus.panache.common.Sort, java.util.Map)} APIs. the previous Spring Data signatures returned {@code org.springframework.data.domain.Page<T>} and accepted {@code org.springframework.data.domain.Pageable}. Those Spring types are gone in Quarkus; the paged finders below now return a Panache {@link PanacheQuery} and ...
-`app/proprietary/src/main/java/stirling/software/proprietary/repository/PersistentAuditEventRepository.java:197` - Find IDs for batch deletion - using JPQL with paging instead of a native query. originally accepted a Spring {@code Pageable}; callers must pass an {@code io.quarkus.panache.common.Page} instead (see class doc).
-`app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/TeamController.java:247` - teamRepository/userRepository still extend Spring Data JpaRepository. Once they are migrated to Panache, findById(...) returns the entity directly (not Optional); update the Optional handling above accordingly. Likewise save(...) -> persist(...), delete(...) -> delete(...)/deleteById(...). Derived finders existsByNameIgnoreCase / countByTeam must be reimplemented as Panache default methods.
-`app/proprietary/src/main/java/stirling/software/proprietary/storage/service/FolderService.java:268` - StoredFileRepository is still a Spring Data JpaRepository; save()/saveAll()/flush() resolve against it for now. When that repository is ported to a Panache repository, map these to persist()/flush() accordingly.
</details>
<details><summary><b>MVC view / template rendering -> Qute or static</b> (14)</summary>
-`app/common/src/main/java/stirling/software/common/util/ErrorUtils.java:10` - server-rendered error view removed; surface via JAX-RS ExceptionMapper. Spring MVC org.springframework.ui.Model has no Quarkus/Jakarta (JAX-RS) drop-in; the method now mutates and returns a plain Map<String, Object> model holder.
-`app/common/src/main/java/stirling/software/common/util/ErrorUtils.java:23` - server-rendered error view removed; surface via JAX-RS ExceptionMapper. Spring MVC org.springframework.web.servlet.ModelAndView has no Quarkus/Jakarta (JAX-RS) drop-in; the method now returns a plain Map<String, Object> model holder instead of a ModelAndView (the incoming model parameter is retained for signature compatibility but is no longer the Spring Model type).
-`app/core/src/main/resources/application.properties:62` - ---- Error handling (was spring.web.error.* / spring.mvc.problemdetails.enabled=false) -------- GlobalExceptionHandler is an @ControllerAdvice; rewrite as JAX-RS ExceptionMapper(s) producing RFC 7807 ProblemDetail responses. The Spring error-page / whitelabel settings below have no Quarkus property equivalent: spring.web.error.path=/error, whitelabel.enabled=false, include-stacktrace/exception/message=always
-`app/proprietary/build.gradle:16` - ---- Spring -> Quarkus extension mapping (full native migration) ---- spring-jdbc -> Agroal datasource (transitive via hibernate-orm). JdbcTemplate usage, if any, must be rewritten to plain JDBC / Panache. replace any org.springframework.jdbc.core.JdbcTemplate usage. spring-webmvc -> quarkus-rest (inherited api-scoped from :common).
-`app/proprietary/build.gradle:30` - spring-boot-starter-data-redis -> quarkus-redis-client (used by the optional Valkey backplane). rewrite RedisTemplate/Lettuce usage on the Quarkus Redis client API.
-`app/proprietary/src/main/java/stirling/software/proprietary/audit/AuditDashboardWebController.java:35` - Spring's org.springframework.ui.Model + view-name ("audit/dashboard") drove Thymeleaf server-side rendering. Quarkus has no Thymeleaf view resolver; the equivalent is a Qute TemplateInstance bound to src/main/resources/templates/audit/dashboard.html. rebind this view to Qute. Inject @io.quarkus.qute.Location("audit/dashboard") io.quarkus.qute.Template dashboard; and return dashboard.data(...) as a TemplateInstance (with a Qute ...
-`app/proprietary/src/main/java/stirling/software/proprietary/audit/AuditDashboardWebController.java:53` - return the rendered Qute template instead of this placeholder once audit/dashboard.html is migrated. The attributes in `model` map 1:1 to the former Spring Model attributes.
-`app/proprietary/src/main/java/stirling/software/proprietary/cluster/valkey/ValkeyClusterBackplane.java:25` - was Spring spring-data-redis StringRedisTemplate. Replaced with Quarkus RedisDataSource (io.quarkus.redis.datasource). Verify the redis client extension (quarkus-redis-client) is on the classpath and configured via quarkus.redis.* properties.
-`app/proprietary/src/main/java/stirling/software/proprietary/cluster/valkey/ValkeyClusterBackplane.java:37` - Original used template.execute() so the connection was borrowed from the pool and returned in a finally block - critical because isHealthy() is hit on every k8s liveness/readiness probe tick. Quarkus RedisDataSource manages connection pooling/return internally, so issuing a single command (PING) is the equivalent. confirm command mapping. Quarkus exposes PING via the low-level command API: redisDataSource.execute("PING") returns ...
-`app/proprietary/src/main/java/stirling/software/proprietary/cluster/valkey/ValkeyConnectionConfiguration.java:86` - MIGRATION: the former @Produces RedisDataSource methods (valkeyConnectionFactory / valkeyTemplate) were removed - they only handed back the container-managed RedisDataSource and produced two @Default beans of the same type, which Arc flagged as an ambiguous dependency for every consumer that injects a plain RedisDataSource. All Valkey* collaborators now inject the Quarkus-provided RedisDataSource directly. the eager boot ...
-`app/saas/src/main/java/stirling/software/saas/ai/controller/AiProxyController.java:155` - Spring's "/output/**" wildcard mapping has no direct JAX-RS equivalent; using a {path:.*} regex template to capture the trailing path segments.
-`app/saas/src/main/java/stirling/software/saas/config/SaasRestTemplateConfig.java:15` - HTTP client for talking to Supabase Edge Functions, with a bounded connect timeout. replaced Spring RestTemplate with java.net.http.HttpClient. Consider a typed {@code@RegisterRestClient} client instead. Note: the per-request read timeout previously set on RestTemplate must now be applied per HttpRequest via {@code HttpRequest.Builder#timeout}.
-`app/saas/src/main/java/stirling/software/saas/service/SaasTeamService.java:43` - Spring RestTemplate replaced with JDK java.net.http.HttpClient for the Supabase edge-function email POST (see sendInvitationEmail).
-`app/saas/src/main/java/stirling/software/saas/service/SaasTeamService.java:705` - Spring RestTemplate (HttpHeaders/MediaType/HttpEntity + postForEntity) replaced with JDK HttpClient. Preserves the JSON POST with the bearer Authorization header to the Supabase edge function.
-`app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java:45` - rebind via @io.smallrye.config.ConfigMapping or @io.quarkus.arc.config.ConfigProperties. Was Spring @ConfigurationProperties(prefix = ""), kept here as a plain CDI bean POJO; the property binding is not yet wired in Quarkus. Spring @Order(Ordered.HIGHEST_PRECEDENCE) controlled configuration-bean ordering; there is no equivalent CDI ordering annotation for this bean.
-`app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java:84` - REMOVED (Spring -> Quarkus): dynamicYamlPropertySource(ConfigurableEnvironment). This was a Spring @Bean that registered settings.yml as an extra runtime PropertySource on the ConfigurableEnvironment (added first, or last under the "saas" profile). Quarkus has no ConfigurableEnvironment/PropertySource model and the @Bean had already been removed, so the method was dead code referencing Spring-only types. reimplement external ...
-`app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesDynamicYamlPropertySourceTest.java:18` - Spring Boot test framework not available in Quarkus
-`app/core/src/main/java/stirling/software/SPDF/SPDFApplication.java:70` - the Spring "spring.config.additional-location" property used to load the external settings/customSettings YAML files into the environment. Quarkus uses SmallRye Config; wire these files via a config source instead, e.g. set the system property "smallrye.config.locations" to the (comma-separated) file: URLs before this point, or register a custom ConfigSourceFactory. The directories/log lines above are preserved.
-`app/core/src/main/java/stirling/software/SPDF/exception/GlobalExceptionHandler.java:114` - development mode used to be derived from Spring active profiles via org.springframework.core.env.Environment. Quarkus exposes the profile through io.quarkus.runtime.LaunchMode / quarkus.profile; this is read here from the standard config so no Spring Environment is needed.
-`app/core/src/main/java/stirling/software/SPDF/exception/GlobalExceptionHandler.java:902` - this replaces Spring's Environment.getActiveProfiles() ("dev"/"development") check. Quarkus exposes the active profile via io.quarkus.runtime.LaunchMode and the "quarkus.profile" config key; read it from the standard config so no Spring Environment bean is required.
-`app/proprietary/src/main/java/stirling/software/proprietary/config/AuditConfigurationProperties.java:16` - Spring @Order(HIGHEST_PRECEDENCE + 10) had no direct CDI equivalent; bean ordering/precedence must be handled via @Priority or explicit ordering at injection points if it was relied upon.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/DatabaseController.java:44` - @Conditional(H2SQLCondition.class) gated this controller on the datasource being H2 (driver/url inspection of the Spring Environment). Quarkus has no @Conditional equivalent; this must be re-expressed either as a build-time @IfBuildProfile, a runtime @LookupIfProperty on a datasource property, or a runtime guard inside DatabaseService that no-ops/returns 404 when the active datasource is not H2.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/supabase/SupabaseUserLoginProperties.java:8` - this was a Spring @ConfigurationProperties(prefix = "security.supabase.user-login") POJO. Rebind the prefixed properties via @io.smallrye.config.ConfigMapping(prefix = "security.supabase.user-login") (interface-based) so the fields are populated from configuration; until then this bean holds defaults only.
-`app/saas/src/main/java/stirling/software/saas/config/SupabaseConfigurationProperties.java:11` - @ConfigurationProperties(prefix="app.supabase"); bind via @ConfigProperty or @ConfigMapping
</details>
<details><summary><b>Spring test framework -> Quarkus test</b> (11)</summary>
-`app/common/src/test/java/stirling/software/common/cluster/InProcessConfigurationConditionalTest.java:20` - Spring Boot test framework not available in Quarkus
-`app/proprietary/src/test/java/stirling/software/proprietary/cluster/s3/S3VendorComprehensiveTest.java:55` - Spring Boot test framework not available in Quarkus
-`app/proprietary/src/test/java/stirling/software/proprietary/cluster/valkey/LiveExternalClusterTest.java:41` - Spring Boot test framework not available in Quarkus
-`app/proprietary/src/test/java/stirling/software/proprietary/cluster/valkey/LiveValkeyAuthIntegrationTest.java:28` - Spring Boot test framework not available in Quarkus
-`app/proprietary/src/test/java/stirling/software/proprietary/cluster/valkey/LiveValkeyChaosTest.java:32` - Spring Boot test framework not available in Quarkus
-`app/proprietary/src/test/java/stirling/software/proprietary/cluster/valkey/LiveValkeyIntegrationTest.java:43` - Spring Boot test framework not available in Quarkus
-`app/proprietary/src/test/java/stirling/software/proprietary/cluster/valkey/ValkeyClusterBackplaneTest.java:25` - Spring Boot test framework not available in Quarkus
-`app/proprietary/src/test/java/stirling/software/proprietary/cluster/valkey/ValkeyConnectionConfigurationTest.java:33` - Spring Boot test framework not available in Quarkus
-`app/proprietary/src/test/java/stirling/software/proprietary/mcp/security/McpApiKeyIntegrationTest.java:36` - Spring Boot test framework not available in Quarkus
-`app/proprietary/src/test/java/stirling/software/proprietary/mcp/security/McpOAuthIntegrationTest.java:58` - Spring Boot test framework not available in Quarkus
-`app/proprietary/src/test/java/stirling/software/proprietary/security/service/MailConfigTest.java:18` - Spring Boot test framework not available in Quarkus
-`app/common/src/main/java/stirling/software/common/configuration/SchedulingConfig.java:15` - Quarkus' {@code quarkus-scheduler} extension owns the scheduling thread pool, so no application bean is required. To keep the "each scheduled task on its own virtual thread" behaviour, annotate the individual {@code@io.quarkus.scheduler.Scheduled} methods with {@code@io.smallrye.common.annotation.RunOnVirtualThread} (or configure {@code quarkus.scheduler.use-virtual-threads=true} where supported). any injection point that ...
-`app/common/src/main/java/stirling/software/common/service/TempFileCleanupService.java:132` - Scheduled task to clean up old temporary files. Runs at the configured interval. the Spring form used a SpEL expression ({@code fixedDelayString="#{applicationProperties.system.tempFileManagement.cleanupIntervalMinutes}"}). Quarkus {@code@Scheduled} cannot reference an arbitrary bean property; {@code every} only resolves a MicroProfile Config placeholder. The cleanup interval must therefore be exposed as a config key (e.g ...
-`app/proprietary/src/main/java/stirling/software/proprietary/cluster/ClusterNodeBootstrap.java:94` - Spring @Scheduled(fixedDelayString = "${cluster.node.heartbeat-interval-ms:5000}") drove the interval directly from config in milliseconds. Quarkus @Scheduled "every" expects a Duration string, so the config reference "{cluster.node.heartbeat-interval-ms}" cannot be reused as-is (it resolves to a bare number). Hard-coded to 5s to match the model default; if the interval is operator-tunable, expose a duration-formatted property ...
-`app/proprietary/src/main/java/stirling/software/proprietary/config/CustomAuditEventRepository.java:46` - was @Async("auditExecutor") (Spring async executor). Quarkus has no @Async; run this off the request thread via a managed executor (e.g. inject org.eclipse.microprofile.context.ManagedExecutor and submit, or annotate with @io.smallrye.common.annotation.Blocking on a reactive path). Logic is kept synchronous for now to avoid changing behavior incorrectly.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/ee/LicenseKeyChecker.java:58` - Spring used initialDelay=fixedRate=7d. Quarkus @Scheduled has no initialDelay equivalent for fixed-rate; "every=P7D" fires the first run 7 days after start, which preserves the original initial-delay semantics. delayed="..." could add an extra offset if needed. MIGRATION: every="7d" was rejected ("Invalid every() expression") because Quarkus parses the value as a Duration and a bare "7d" maps to the invalid "PT7d". Use the ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/database/ScheduledTasks.java:23` - the original bean used @Conditional(H2SQLCondition.class) to skip registration entirely when not running on H2. Quarkus has no runtime @Conditional, so the gate is evaluated at runtime here via h2SQLCondition.matches() and the backup is short-circuited when false. The schedule still fires on the configured cron but becomes a no-op off H2. the Spring cron was a SpEL expression ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/service/EmailService.java:46` - Spring's @Async ran this on a managed executor. Quarkus has no @Async; the method now runs synchronously on the caller's thread. To restore async behaviour wrap the body in io.smallrye.mutiny.Uni or submit to a jakarta.enterprise.concurrent ManagedExecutor (would change the void signature, so deferred).
-`app/proprietary/src/main/java/stirling/software/proprietary/security/service/EmailService.java:100` - @Async dropped (no Quarkus equivalent); now runs synchronously.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/service/EmailService.java:125` - @Async dropped (no Quarkus equivalent); now runs synchronously.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/service/EmailService.java:151` - @Async dropped (no Quarkus equivalent); now runs synchronously.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/service/EmailService.java:211` - @Async dropped (no Quarkus equivalent); now runs synchronously.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/service/EmailService.java:257` - @Async dropped (no Quarkus equivalent); now runs synchronously.
-`app/proprietary/src/main/java/stirling/software/proprietary/service/AiUserDataService.java:33` - Spring's @Async ran this fire-and-forget on a managed executor so an unavailable engine never delayed the logout response. Quarkus has no @Async; the method now runs synchronously on the caller's thread. To restore off-thread dispatch, inject a jakarta.enterprise.concurrent.ManagedExecutorService (or annotate the calling REST endpoint with @io.smallrye.common.annotation.RunOnVirtualThread). Errors are still swallowed, so the ...
-`app/saas/src/main/java/stirling/software/saas/payg/job/StaleJobCloser.java:49` - was configurable via property payg.job.stale-close-interval-ms (default 60000ms). io.quarkus.scheduler.Scheduled#every is a fixed string; restore configurability with @Scheduled(every = "{payg.job.stale-close-interval}") + a Duration config property if the interval must stay tunable.
-`app/proprietary/src/main/java/stirling/software/proprietary/config/AuditJpaConfig.java:6` - Quarkus enables transaction management automatically (Narayana/JTA via quarkus-narayana-jta); the Spring @EnableTransactionManagement is not needed. Use jakarta.transaction.@Transactional on methods/beans as required. Scheduling is enabled on the application — no duplicate @EnableScheduling needed. JPA repositories are auto-discovered by Quarkus (no @EnableJpaRepositories needed).
-`app/saas/src/main/java/stirling/software/saas/ai/controller/AiCreateController.java:133` - @Transactional(readOnly = true): jakarta.transaction.Transactional has no readOnly attribute; using a plain transaction.
-`app/saas/src/main/java/stirling/software/saas/ai/controller/AiCreateController.java:143` - @Transactional(readOnly = true): jakarta.transaction.Transactional has no readOnly attribute; using a plain transaction.
-`app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:59` - replaces Spring TransactionAspectSupport. Used to mark the current jakarta @Transactional transaction rollback-only without propagating the exception.
-`app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:123` - Caller-fixable failures (already-accepted, expired, email mismatch, etc.). Mark the transaction for rollback so anything the service did is reversed even though we don't propagate the exception out of the @Transactional method. replace Spring TransactionAspectSupport rollback-only with jakarta TransactionSynchronizationRegistry.setRollbackOnly() (injected).
-`app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java:753` - returns org.springframework.core.io.Resource, a public signature relied on by callers. Converting to InputStream/byte[]/java.nio would ripple to those call sites, so the Spring Resource type is retained for now.
-`app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java:766` - returns org.springframework.core.io.Resource, a public signature relied on by callers. Converting to InputStream/byte[]/java.nio would ripple to those call sites, so the Spring Resource type is retained for now.
-`app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java:779` - returns org.springframework.core.io.Resource, a public signature relied on by callers. Converting to InputStream/byte[]/java.nio would ripple to those call sites, so the Spring Resource type is retained for now.
-`app/common/src/main/java/stirling/software/common/model/MultipartFile.java:24` - service layer relies on (it exposes {@code org.jboss.resteasy.reactive.multipart.FileUpload} at the REST boundary instead). To avoid rewriting the public signatures of dozens of service and util methods across every module, this interface mirrors the subset of Spring's API that the codebase actually uses. Controllers adapt the inbound {@code FileUpload}/{@code byte[]} to one of the implementations ({@link ...
-`app/common/src/main/java/stirling/software/common/util/misc/CustomColorReplaceStrategy.java:30` - MultipartFile is the constructor parameter type that must match the parent ReplaceAndInvertColorStrategy(MultipartFile, ReplaceAndInvert) constructor (not in scope for this migration). There is no JAX-RS drop-in for this widely used public signature; retained until the parent and its callers are migrated together.
-`app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java:88` - MIGRATION (Spring -> JAX-RS): adapt the inbound multipart uploads to the migration shim MultipartFile so they can be passed to the existing service layer. PipelineProcessor.generateInputFiles still declares the Spring org.springframework.web.multipart.MultipartFile[] parameter type. When that collaborator is migrated to stirling.software.common.model.MultipartFile[], this array type lines up. Until then this controller will not ...
-`app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertPdfToCbzRequest.java:30` - controller binds this model via @BeanParam multipart. The 'fileInput' field is a raw FileUpload for form binding; the controller must adapt it to a stirling.software.common.model.MultipartFile via FileUploadMultipartFile.of(fileInput).
-`app/proprietary/src/main/java/stirling/software/proprietary/service/ByteHashFileIdStrategy.java:17` - the FileIdStrategy interface (collaborator file) still imports org.springframework.web.multipart.MultipartFile; it must be switched to stirling.software.common.model.MultipartFile so this implementation's signature matches.
-`app/proprietary/src/main/java/stirling/software/proprietary/storage/controller/FileStorageController.java:91` - storeFileResponse(...) still accepts Spring org.springframework.web.multipart.MultipartFile. Migrate FileStorageService to accept stirling.software.common.model.MultipartFile, then this wrapping is type-compatible.
-`app/proprietary/src/main/java/stirling/software/proprietary/storage/controller/FileStorageController.java:111` - updateFileResponse(...) still accepts Spring MultipartFile; migrate FileStorageService to stirling.software.common.model.MultipartFile.
</details>
<details><summary><b>Caching</b> (2)</summary>
-`app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/CacheConfig.java:8` - Spring's @EnableCaching + a programmatic CaffeineCacheManager @Bean has no direct Quarkus equivalent. Quarkus caching is annotation-driven (io.quarkus.cache.@CacheResult / @CacheInvalidate / @CacheName) and configured declaratively in application.properties, e.g.: quarkus.cache.caffeine."<cache-name>".maximum-size=1000 quarkus.cache.caffeine."<cache-name>".expire-after-write=<keyRetentionDays>D ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/service/KeyPersistenceService.java:54` - Spring's CacheManager/Cache("verifyingKeys") has no direct Quarkus-cache equivalent (io.quarkus.cache.Cache cannot enumerate its values). A directly-managed Caffeine cache preserves put/get/evict semantics.
-`app/proprietary/build.gradle:36` - REMOVED: spring-session-core - Quarkus has no Spring Session. Server-side session state (SessionPersistentRegistry, SessionRegistry) must be rewritten on Quarkus' HTTP session (quarkus-undertow servlet session) or a custom store. port Spring Session usage (session registry / persistence).
-`app/common/build.gradle:79` - Jackson 3 (tools.jackson) - retained because ~100 files migrated to the Jackson 3 namespace under Spring Boot 4. Quarkus integrates Jackson 2 for REST bodies; Jackson 3 coexists here as a plain library so those files compile and can still build/parse JSON directly. api-scoped so downstream modules (core, proprietary, saas) that import tools.jackson inherit it. converge the codebase on a single Jackson major version.
-`app/common/src/main/java/stirling/software/common/configuration/AppConfig.java:101` - MIGRATION: many beans inject tools.jackson.databind.ObjectMapper (Jackson 3, inherited from Spring Boot 4). Quarkus' container only produces a com.fasterxml.jackson (Jackson 2) ObjectMapper for REST (de)serialization, so the Jackson 3 type is an unsatisfied CDI dependency. This producer supplies a single application-scoped Jackson 3 mapper built the same way the codebase builds them ad hoc (JsonMapper.builder().build()). REST bodies still go through ...
-`app/common/src/main/java/stirling/software/common/model/io/Resource.java:17` - public method signatures across the codebase that accept or return {@code Resource}, this interface mirrors the subset of Spring's API the codebase actually uses ({@code getInputStream/exists/getFile/getFilename/contentLength/isFile}) together with the {@link FileSystemResource}, {@link InputStreamResource} and {@link ClassPathResource} implementations. Converting a file is then just an import swap. longer term, prefer {@code ...
-`app/common/src/main/java/stirling/software/common/service/InternalApiClient.java:273` - Resolve the port lazily so desktop mode dispatches to the actual bound port. verify Quarkus exposes the bound port via config. Quarkus uses "quarkus.http.port" and, for random-port test/dev runs, "quarkus.http.test-port"; the old "local.server.port"/"server.port" keys came from Spring Boot's WebServerInitializedEvent.
-`app/common/src/main/java/stirling/software/common/service/JobQueue.java:29` - the original class implemented Spring's SmartLifecycle, which has no direct Quarkus equivalent. start() is now driven by a StartupEvent observer and stop() by @PreDestroy. The SmartLifecycle phase/auto-startup ordering semantics (getPhase()==10) cannot be expressed in CDI; if precise startup/shutdown ordering relative to other beans is required, revisit using @Priority on the observer or @io.quarkus.runtime.Startup with an ...
-`app/common/src/main/java/stirling/software/common/util/GeneralUtils.java:258` - ResourcePatternUtils} pattern resolver. The {@code ResourceLoader} parameter was removed. {@code file:} patterns are resolved with {@link java.nio.file.Files#list}; {@code classpath:} patterns are resolved via the classloader and only support directory resources that live on the filesystem. {@code classpath:} resolution does not enumerate entries inside a packaged JAR. For uber-jar deployments, prefer serving these assets from ...
-`app/common/src/main/java/stirling/software/common/util/SpringContextHolder.java:66` - Spring looked up by bean name across all types; here we resolve a @Named CDI bean of Object.class. Verify named beans are registered with a matching @jakarta.inject.Named qualifier so this lookup resolves the intended bean.
-`app/core/src/main/java/stirling/software/SPDF/SPDFApplication.java:78` - profile auto-detection (former getActiveProfile / Spring setAdditionalProfiles) must be expressed via "quarkus.profile". The classpath-shape detection logic is retained below in getActiveProfile(); translate its result into the "quarkus.profile" system property (e.g. System.setProperty("quarkus.profile", ...)) before Quarkus.run if profile-based config layering is required.
-`app/core/src/main/java/stirling/software/SPDF/SPDFApplication.java:171` - the Spring "local.server.port" property exposed the actual runtime port (relevant for server.port=0 / "auto" port assignment). In Quarkus read the resolved port from config "quarkus.http.port" (or observe an HTTP-started event) and update serverPortStatic here. Falling back to the configured value for now.
-`app/core/src/main/java/stirling/software/SPDF/config/AppUpdateService.java:31` - MIGRATION: Spring's request-scoped boolean bean -> @Dependent. A CDI normal scope (@RequestScoped) requires a client proxy, which is impossible for a primitive producer ("Producer method for a normal scoped bean must not have a primitive type"). @Dependent recomputes the value at each injection point, the closest behaviour to per-request evaluation. if true per-HTTP-request semantics are needed, wrap the value in a ...
-`app/core/src/main/java/stirling/software/SPDF/config/InitialSetup.java:21` - Spring @Order(Ordered.HIGHEST_PRECEDENCE + 1) controlled the relative order of this startup hook against other initializers. CDI StartupEvent observers have no portable total ordering; if a specific run-before/run-after relationship is required, use @Priority on the observer parameter or @Observes(during=...) and coordinate ordering across the migrated startup beans.
-`app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PrintFileController.java:42` - endpoint mapping was commented out in the original Spring source (the @PostMapping/@Operation were disabled), so this route remains intentionally inactive. The conversion below preserves the disabled state: routing annotations are kept commented. To enable, uncomment the JAX-RS annotations and provide a multipart-bound request. @POST@jakarta.ws.rs.Path("/print-file") @jakarta.ws.rs.Consumes(MediaType.MULTIPART_FORM_DATA) ...
-`app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java:52` - server.servlet.context-path has no direct Quarkus equivalent (it maps to quarkus.http.root-path at build time). Kept as a configurable property so the index.html base href rewrite still works. consider sourcing this from quarkus.http.root-path instead.
-`app/core/src/main/java/stirling/software/SPDF/exception/GlobalExceptionHandler.java:554` - Build the JSON body previously written directly to the servlet response when the client's Accept header could not be satisfied (Spring's {@code HttpMediaTypeNotAcceptableException}). this path was triggered by Spring MVC content negotiation. Under Quarkus/JAX-RS the equivalent is {@code jakarta.ws.rs.NotAcceptableException}; a collaborator should register a mapper that returns this body with status 406 and Content-Type ...
-`app/core/src/main/resources/application.properties:113` - ---- Jackson (was spring.jackson.*) ---------------------------------------------------------- spring.jackson.deserialization.fail-on-null-for-primitives=false no Quarkus property for FAIL_ON_NULL_FOR_PRIMITIVES; register a CDI io.quarkus.jackson.ObjectMapperCustomizer that disables that DeserializationFeature.
-`app/core/src/main/resources/application.properties:132` - ---- External config files ------------------------------------------------------------------- SPDFApplication injected external settings.yml / custom settings via spring.config.additional-location. Quarkus uses a different config-source mechanism (SmallRye Config / quarkus.config.locations). Port ConfigInitializer accordingly.
-`app/proprietary/build.gradle:96` - JDBC drivers via Quarkus extensions (wire into the Agroal datasource). NOTE: H2 is pinned to 2.3.232 because the on-disk file format is incompatible with 2.4.x and upgrading would break existing user databases. quarkus-jdbc-h2's BOM-managed H2 version may differ, so the explicit pin is forced below to preserve file compatibility. verify the H2 version Quarkus resolves still reads 2.3.232 files.
-`app/proprietary/src/main/java/stirling/software/proprietary/audit/AuditAspect.java:114` - Only create the map once we know we'll use it createBaseAuditData must accept InvocationContext (ctx) once AuditService is migrated off ProceedingJoinPoint.
-`app/proprietary/src/main/java/stirling/software/proprietary/audit/AuditAspect.java:124` - addFileData must accept InvocationContext (ctx).
-`app/proprietary/src/main/java/stirling/software/proprietary/audit/AuditAspect.java:148` - addMethodArguments must accept InvocationContext (ctx).
-`app/proprietary/src/main/java/stirling/software/proprietary/audit/AuditAspect.java:199` - resolveEventType reads joinPoint.getTarget(); once AuditService is migrated it should use ctx.getTarget().getClass() instead.
-`app/proprietary/src/main/java/stirling/software/proprietary/audit/ControllerAuditAspect.java:89` - this single {@code@AroundInvoke} replaces the five Spring {@code@Around} advice (GET/POST/PUT/DELETE/PATCH + AutoJobPostMapping) and the static-resource {@code execution(...)} advice. Because CDI cannot inspect Spring/JAX-RS mapping annotations to derive the HTTP verb at bind time, the verb is resolved from the live request ({@link HttpServletRequest#getMethod()}); if the request is unavailable (non-web invocation) it falls ...
-`app/proprietary/src/main/java/stirling/software/proprietary/audit/ControllerAuditAspect.java:315` - Fallback: try JAX-RS @Path annotation on method/class; return empty string if not present resolve path from jakarta.ws.rs.@Path on the declaring class and method once all controllers are fully on JAX-RS. The Spring fallback was removed.
-`app/proprietary/src/main/java/stirling/software/proprietary/cluster/ClusterLicenseGate.java:20` - Runtime license gate for cluster mode. Cluster mode requires a SERVER or ENTERPRISE license; the SaaS flavor bypasses (no {@code runningProOrHigher} bean is published). The Valkey connection config {@code@DependsOn} this bean, so it runs before any Valkey bean is constructed. Spring @DependsOn ordering relative to the Valkey connection config has no direct Quarkus equivalent. Ensure the Valkey/Redis bean either @Inject's this ...
-`app/proprietary/src/main/java/stirling/software/proprietary/cluster/ClusterNodeBootstrap.java:35` - Integer.MAX_VALUE} so Spring tore this bean down before {@code LettuceConnectionFactory} - deregister therefore ran while the Valkey connection was still alive. Quarkus has no SmartLifecycle/getPhase shutdown-ordering equivalent. Startup now runs via @Observes StartupEvent and shutdown via @PreDestroy. If the Quarkus Redis/Valkey client is torn down before this bean's @PreDestroy, the deregister call may fail (it already ...
-`app/proprietary/src/main/java/stirling/software/proprietary/cluster/valkey/ValkeyConnectionConfiguration.java:69` - in Quarkus the RedisDataSource is produced by the quarkus-redis-client extension from quarkus.redis.* config rather than constructed here. This producer simply hands back the container-managed RedisDataSource so existing @Inject points keep compiling. The URL/TLS validation that used to build the LettuceConnectionFactory is still performed (and the boot handshake attempted) so misconfiguration fails fast.
-`app/proprietary/src/main/java/stirling/software/proprietary/cluster/valkey/ValkeyConnectionConfiguration.java:177` - Bound every backplane command. Without this a partitioned or slow Valkey would stall hot-path calls (e.g. JobController.guardNonOwner -> jobStore.get on each request); all backplane ops are non-blocking single commands, so a short timeout is safe. propagate this to quarkus.redis.timeout=2s.
-`app/proprietary/src/main/java/stirling/software/proprietary/cluster/valkey/ValkeyConnectionConfiguration.java:191` - 10 x 3s = 30s boot-time retry. Auth failures (WRONGPASS/NOAUTH/NOPERM) short-circuit immediately; only transport errors get the loop. Package-private for testing. this previously issued PING via a spring-data-redis RedisConnection. With Quarkus it should issue {@code ds.execute("PING")} (string command). The loop structure and auth short-circuit are retained; the actual ping call is stubbed so the file compiles until the ...
-`app/proprietary/src/main/java/stirling/software/proprietary/cluster/valkey/ValkeyConnectionConfiguration.java:252` - replace with ds.execute("PING").toString() (or the typed RedisDataSource command API) once the Quarkus command surface for the backplane is wired.
-`app/proprietary/src/main/java/stirling/software/proprietary/cluster/valkey/ValkeyConnectionConfiguration.java:302` - MIGRATION: Bucket4j's Lettuce ProxyManager (ValkeyRateLimitStore) needs a raw io.lettuce.core.RedisClient, which Quarkus' redis extension does not expose. Produce one from the same cluster.valkey.url the rest of the backplane uses so the injection point for AbstractRedisClient resolves. Only active when the Valkey backplane is selected. propagate password/TLS auth from the parsed endpoint onto the RedisURI once cluster.valkey ...
-`app/proprietary/src/main/java/stirling/software/proprietary/cluster/valkey/ValkeyRateLimitStore.java:39` - this previously received a spring-data-redis LettuceConnectionFactory (produced by the not-yet-migrated ValkeyConnectionConfiguration) and unwrapped its native io.lettuce.core.RedisClient. Bucket4j's Lettuce ProxyManager only needs that raw RedisClient. Once ValkeyConnectionConfiguration is migrated to a Quarkus producer (exposing a RedisClient or io.quarkus.redis.datasource.RedisDataSource), inject it here directly and drop the ...
-`app/proprietary/src/main/java/stirling/software/proprietary/config/CustomAuditEventRepository.java:22` - this class implemented Spring Boot Actuator's org.springframework.boot.actuate.audit.AuditEventRepository (with @Primary). Quarkus has no Actuator equivalent, so the interface and the org.springframework.boot.actuate.audit.AuditEvent type are gone. The write side has been ported to a plain CDI bean that accepts the audit data directly (see add(...) below). Whatever Spring code previously published AuditEvents to this repository ...
-`app/proprietary/src/main/java/stirling/software/proprietary/config/CustomAuditEventRepository.java:90` - repo.persist(...) depends on PersistentAuditEventRepository being migrated to a Quarkus PanacheRepository (save -> persist). Update this call once that collaborator is converted.
-`app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AiEngineController.java:77` - SSE stream timeout (ms), long enough for multi-gigabyte PDF workflows without completing out from under the executor. Derived from {@code aiEngine.streamTimeoutSeconds}. the JAX-RS SSE API has no per-emitter timeout equivalent to Spring's {@code SseEmitter} constructor argument. Enforce this timeout against the background orchestration task (e.g. a scheduled cancellation / Future.get with timeout) if a hard cap is required; for ...
-`app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AuditDashboardController.java:68` - PersistentAuditEventRepository is a collaborator that must be migrated to io.quarkus.hibernate.orm.panache.PanacheRepositoryBase<PersistentAuditEvent, Long>. Its paged finders should return io.quarkus.panache.common.PanacheQuery (or apply the Page/Sort built here) instead of org.springframework.data.domain.Page. The pagination request below is expressed with Panache Page/Sort; once the repository accepts these the ...
-`app/proprietary/src/main/java/stirling/software/proprietary/mcp/McpServerController.java:112` - Spring's @ExceptionHandler(HttpMessageNotReadableException.class) wrapped malformed-JSON failures as a JSON-RPC Parse error. In JAX-RS this maps to a jakarta.ws.rs.ext.ExceptionMapper provider. move this handling to a @Provider ExceptionMapper<...> (e.g. mapping the JSON deserialization exception thrown by the Jackson MessageBodyReader) returning HTTP 400 with JsonRpcResponse.failure(null, JsonRpcError.parseError("Request body ...
-`app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpSecurityConfig.java:95` - the following describe the original chain wiring so the Quarkus re-implementation can reproduce it faithfully. They are documented as notes rather than executable HttpSecurity DSL (which does not exist in Quarkus).
-`app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowRequest.java:33` - conversationHistory is a list of POJOs; RESTEasy has no form converter for AiConversationMessage. It must be received as a JSON form part (e.g. a String field parsed with ObjectMapper, or a @RestForm@PartType(APPLICATION_JSON) field) once the multipart contract for this endpoint is finalised.
-`app/proprietary/src/main/java/stirling/software/proprietary/model/api/audit/AuditDateExportRequest.java:27` - Spring @DateTimeFormat(iso = ISO.DATE) removed; JAX-RS binds LocalDate via its default ISO-8601 (yyyy-MM-dd) ParamConverter, so ISO.DATE form values still bind. If a non-ISO format is ever needed, register a jakarta.ws.rs.ext.ParamConverter.
-`app/proprietary/src/main/java/stirling/software/proprietary/repository/PersistentAuditEventRepository.java:46` - --------------------------------------------------------------------- Basic paged queries callers must adapt to the PanacheQuery return type (see class doc). ---------------------------------------------------------------------
-`app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/DatabaseConfig.java:129` - the Spring @ConditionalOnBooleanProperty(name = "premium.enabled") gate is not expressible on a private helper under CDI. The custom-database path is already guarded at runtime by the runningProOrHigher + datasource.enableCustomDatabase checks in dataSource(); if a separate premium.enabled toggle is still required, read it via org.eclipse.microprofile.config.Config (e.g. premium.enabled) inside dataSource() before calling this ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/MailConfig.java:16` - This configuration class used to provide the Spring JavaMailSender bean. After the Quarkus migration, mail sending is handled by Quarkus' built-in {@code io.quarkus.mailer.Mailer}, which is auto-provided by the quarkus-mailer extension and injected directly where needed (e.g. in EmailService). There is therefore no longer a producer method here. the SMTP connection settings previously configured programmatically from {@link ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/PasswordEncoderConfig.java:23` - replace BCryptPasswordEncoder once a Quarkus-compatible BCrypt implementation is wired in (see class-level note).
-`app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java:277` - Produces the persistent remember-me token repository. {@link JPATokenRepositoryImpl} implements the Spring Security {@code PersistentTokenRepository} interface (collaborator not yet migrated). The remember-me feature itself has no Quarkus equivalent (see class javadoc); the repository is still produced so the persistence logic is available to the reimplementation. Producer return type narrowed to the concrete class to avoid ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/EmailController.java:92` - Catches any messaging exception (e.g., invalid email address, SMTP server issues). the Spring-specific org.springframework.mail.MailSendException ("Invalid Addresses" case) was previously handled separately. Once EmailService is migrated off Spring's JavaMailSender that branch can be reintroduced with the replacement exception type.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UserController.java:258` - Spring's SecurityContextLogoutHandler has no Quarkus equivalent. Session/logout handling must be re-implemented via the migrated session registry (expire the current session) and/or quarkus auth config.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UserController.java:346` - Spring's SecurityContextLogoutHandler has no Quarkus equivalent. Re-implement logout via the migrated session registry / quarkus auth config.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UserController.java:391` - Spring's SecurityContextLogoutHandler has no Quarkus equivalent. Re-implement logout via the migrated session registry / quarkus auth config.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/enterprise/DatabaseControllerEnterprise.java:24` - @Conditional(H2SQLCondition.class) had no direct Quarkus equivalent. H2SQLCondition is an org.springframework.context.annotation.Condition that inspects active profiles and datasource URL/type at bean-registration time. Quarkus has no equivalent for an arbitrary runtime Condition deciding whether to register a JAX-RS resource. Options: gate the endpoints with @io.quarkus.arc.lookup.LookupIfProperty / ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/database/H2SQLCondition.java:10` - this was an org.springframework.context.annotation.Condition used via @Conditional(H2SQLCondition.class) to gate bean/controller registration at startup. Quarkus has no runtime @Conditional equivalent (@io.quarkus.arc.profile.IfBuildProfile / @LookupIfProperty are build-time/property-name based and cannot replicate this composite logic). The decision logic has been preserved as a runtime-evaluable CDI bean; callers that ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/service/DatabaseService.java:317` - dropped catch for org.springframework.jdbc.datasource.init.CannotReadScriptException (Spring JDBC). Raw JDBC PreparedStatement.execute() only throws SQLException; the missing-file case is now reported via the SQLException branch above. Restore equivalent handling if a Quarkus/Hibernate script runner is introduced later.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/service/DatabaseService.java:511` - dropped catch for org.springframework.jdbc.datasource.init.ScriptException (Spring JDBC). Raw JDBC PreparedStatement.execute() only throws SQLException; script errors are now logged via the SQLException branch above. Restore equivalent handling if a Quarkus/Hibernate script runner is introduced later.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/service/KeyPairCleanupService.java:29` - Spring @ConditionalOnBooleanProperty("v2") dropped; the "v2" runtime toggle has no direct CDI equivalent. Guard activation via a runtime check or @io.quarkus.arc.lookup.LookupIfProperty / quarkus.scheduler config if this bean should be conditionally enabled.
-`app/proprietary/src/main/java/stirling/software/proprietary/security/service/UserService.java:82` - org.springframework.context.MessageSource and LocaleContextHolder (Spring i18n) have no Quarkus equivalent on the classpath. Rebind to a Quarkus message bundle (io.quarkus.qute / @org.eclipse.microprofile.config or a jakarta.enterprise localization helper) and an explicit Locale source. The injected field is removed for now and getInvalidUsernameMessage() returns a constant fallback so the bean can be constructed; localization ...
-`app/proprietary/src/main/java/stirling/software/proprietary/security/service/UserService.java:632` - was messageSource.getMessage("invalidUsernameMessage", null, LocaleContextHolder.getLocale()). Spring's MessageSource / LocaleContextHolder are not on the Quarkus classpath; rebind to a Quarkus localization mechanism (message bundle + request Locale) and restore the localized lookup. Returning the message key as a fallback preserves behavior shape until i18n is ported.
-`app/proprietary/src/main/java/stirling/software/proprietary/workflow/dto/SignDocumentRequest.java:80` - wetSignatures is a parsed list of POJOs populated by the controller/service from wetSignaturesData, not bound directly from the form; RESTEasy has no converter for WetSignatureMetadata, so it is intentionally left without @RestForm.
-`app/saas/src/main/java/stirling/software/saas/ai/service/AiCreateSessionService.java:39` - Spring MVC RequestContextHolder/ServletRequestAttributes replaced with a CDI-injected request-scoped HttpServletRequest (quarkus-undertow). Wrapped in Instance so resolution outside an active HTTP request (e.g. scheduled/startup contexts) is a safe no-op.
-`app/saas/src/main/java/stirling/software/saas/config/SaasDataSourceConfig.java:12` - SaaS-profile Postgres datasource configuration. datasource/JPA now configured via quarkus.datasource.* / quarkus.hibernate-orm.* in application.properties. The former Hikari-based DataSource bean (Postgres, @Primary over the OSS H2 default) translates to Quarkus config, e.g.: <pre> quarkus.datasource.db-kind=postgresql quarkus.datasource.username=${SPRING_DATASOURCE_USERNAME:postgres} ...
-`app/saas/src/main/java/stirling/software/saas/config/SaasJpaConfig.java:10` - Previously registered the {@code :saas} module's entities and repositories with Spring Data JPA. datasource/JPA now configured via quarkus.datasource.* / quarkus.hibernate-orm.* in application.properties. Entity scanning and repository discovery are automatic in Quarkus (Panache/Hibernate ORM), so the former @EnableJpaRepositories basePackages (stirling.software.saas.repository, .billing.repository, .ai.repository ...
-`app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:131` - replace Spring TransactionAspectSupport rollback-only with jakarta TransactionSynchronizationRegistry.setRollbackOnly() (injected).
-`app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:418` - replace Spring TransactionAspectSupport rollback-only with jakarta TransactionSynchronizationRegistry.setRollbackOnly() (injected).
-`app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:426` - replace Spring TransactionAspectSupport rollback-only with jakarta TransactionSynchronizationRegistry.setRollbackOnly() (injected).
-`app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:467` - replace Spring TransactionAspectSupport rollback-only with jakarta TransactionSynchronizationRegistry.setRollbackOnly() (injected).
-`app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:475` - replace Spring TransactionAspectSupport rollback-only with jakarta TransactionSynchronizationRegistry.setRollbackOnly() (injected).
-`app/saas/src/main/java/stirling/software/saas/payg/lineage/LineagePruneScheduler.java:47` - Spring 6-field cron "0 0 ****" (top of every hour) translated to Quartz cron "0 0 * ? * *" (day-of-month set to ? per Quartz day-of-week/day-of-month mutual-exclusion). Configurability is preserved via the {payg.lineage.prune-cron} config expression; set that property to a Quartz-syntax cron (default below) to override.
-`app/saas/src/main/java/stirling/software/saas/payg/policy/PolicyChangedEvent.java:12` - was a Spring ApplicationEvent subclass. Converted to a plain POJO CDI event (no `extends ApplicationEvent`, no super(source) call). The `source` is retained as a plain field so the existing (Object source, String payload) constructor used by PricingPolicyService stays source-compatible.
-`app/saas/src/main/java/stirling/software/saas/payg/policy/PricingPolicyService.java:198` - after-commit delivery is now expressed on the observer side via CDI's TransactionPhase.AFTER_SUCCESS (replacing the firing-side TransactionSynchronizationManager.registerSynchronization afterCommit hook). When no transaction is active (e.g. test paths calling write methods without a tx), CDI delivers the event immediately, matching the former else-branch behavior.
-`app/saas/src/main/java/stirling/software/saas/payg/policy/PricingPolicyService.java:227` - was TransactionSynchronizationManager-driven after-commit dispatch; now a plain Event.fire() whose after-commit timing is enforced by the AFTER_SUCCESS observer phase on onPolicyChanged.
-`app/saas/src/main/java/stirling/software/saas/payg/test/PaygCucumberThrowController.java:65` - declared return type was ResponseEntity<Void> so the AutoJobAspect @Around 500 reached the wire (see class javadoc). Verify the JAX-RS return-value handling of Response preserves the advice's 500 status under Quarkus.
-`app/saas/src/main/java/stirling/software/saas/service/SupabaseUserService.java:45` - Spring Data save() did an upsert (merge); SupabaseUser uses an assigned UUID id and this path updates an existing row, so use EntityManager.merge to preserve update-or-insert semantics rather than Panache persist (INSERT-only).
-`build.gradle:275` - Jackson integration for JAX-RS bodies. Quarkus integrates Jackson 2 (com.fasterxml). 100 files import tools.jackson (Jackson 3, from Spring Boot 4). Jackson 2 and 3 can coexist (different namespaces); the Jackson 3 dependency is retained in app/common so those files still compile, but REST (de)serialization goes through Quarkus' Jackson 2 ObjectMapper. Converge on one Jackson line later.
</details>
## Disabled tests
16 test classes carry `@Disabled("Spring Boot test framework not available in Quarkus")`. They need
rewriting against `@QuarkusTest` / `@QuarkusComponentTest`, or plain JUnit where the test does not
actually need a container. Find them with:
```bash
grep -rn 'Spring Boot test framework not available in Quarkus' app/
```
The root `build.gradle` additionally excludes, from the `test` source set, any test whose text
contains `import org.springframework` or `import com.nimbusds`, plus an explicit
`quarkusMigrationExcludedTests` list. Two gaps in that filter to know about:
- It matches on the **import line only**. A test that references `org.springframework.mock.web`
fully qualified, or breaks via `org.springdoc`, or reflects onto a field whose type the migration
changed, sails straight through and has to be added to the explicit list by hand.
- Because the exclusion is on the source set, an excluded test is not compiled either - so
`compileTestJava` passing is not evidence that the test still matches the code.
Both mechanisms should be deleted once the list is empty.
## Known functional gaps
- **`@ToolIO` is missing from the build-time OpenAPI schema.** `ToolIOOperationCustomizer` is an
`OASFilter` that runs at `RUNTIME_STARTUP`, because it reads `ToolIORegistry` and that only exists
once the container is up. The schema exported during augmentation
(`quarkus.smallrye-openapi.store-schema-directory`) therefore carries no `x-stirling-io`, which is
what the frontend type generator and the AI engine's `generate_tool_models.py` read. Reading the
declarations from the Jandex index in a build step would restore it.
- **Endpoint enumeration discovers nothing.** `EndpointInspector`, `McpToolCatalog` and
`AiEngineEndpointResolver` all relied on Spring's `RequestMappingHandlerMapping` to list handlers,
and currently fall back to wildcards or empty catalogues. `ToolIORegistry` shows a working
replacement on Quarkus: walk the CDI beans via `BeanManager`, read the JAX-RS `@Path` annotations
off the class and its methods. The same approach fits all three.
- **Only one `OASFilter` can be registered through `mp.openapi.filter`.** `application.properties`
uses it for `ToolModelSchemaCustomizer`, so `OpenApiConfig` and `GlobalErrorResponseCustomizer`
are ported but never invoked - the API `Info`, the global `AI` tag, the `apiKey` security scheme
and the shared error responses are all absent from the published spec. Register the extra filters
with `@io.quarkus.smallrye.openapi.OpenApiFilter` instead.
- **Fingerprint-based session management is gone.** All three classes in
`app/core/src/main/java/stirling/software/SPDF/config/fingerprint/` are commented out top to
// -PjpdfiumPlatforms=auto|all|none|<csv of linux-x64,linux-arm64,linux-musl-x64,linux-musl-arm64,darwin-x64,darwin-arm64,windows-x64> (windows-arm64 natives not published yet)
// MIGRATION (Spring->JAX-RS): controllers using this annotation must declare
// @jakarta.ws.rs.Path("/api/v1/invite").
// JAX-RS does not honour @Path via meta-annotations, so the path is not inherited from here.
@Tag(
name="Invite",
description=
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.