Enhance backend stability, security, and type safety - #19
Conversation
\DB::transaction() begins on the default management connection while every Content/ContentVersion write inside goes through the per-space connection, so the transaction guarded nothing: a failure mid-publish or mid-reorder left the tenant database permanently inconsistent. All 19 content/release sites now use the SerialAllocator idiom ($model->getConnection()->transaction()), and the menu-cache afterCommit hooks register on that same connection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgfZsao7eVj1Vo636nZQwS
The ambient currentSpace binding set by the delivery API and other
request paths was never cleared between requests on a long-lived
worker, and the resolver fell back to it silently — flushing it via
octane.flush closes that. The resolver also read request('space'),
which checks query/body input before the route, so ?space=x could
displace the bound model; it now accepts only the resolved route
parameter before falling back to SpaceContext.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FgfZsao7eVj1Vo636nZQwS
Token abilities were stored and shown in the UI but never checked on the delivery API, so any embedded token could fetch every unpublished draft via vid=draft. The middleware now requires a per-resource read grant and a new preview ability for non-published version scopes. Already-issued tokens keep exactly their old surface: a data migration grandfathers them to *:read + *:preview. New tokens default to published-only reads, with an "Allow draft preview" checkbox on the token settings page, and abilities validation is re-enabled on create. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgfZsao7eVj1Vo636nZQwS
The typed i18n schema surfaced the drift: the renamed password-reset flow, the AI-config settings, spaces.empty, actions.view.view and two aiSettings toasts were missing in German, while the old reset-flow keys and the superseded reference/multiAsset field-type entries were German-only orphans. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgfZsao7eVj1Vo636nZQwS
Production had no error tracking at all: the reportable() hook was an empty closure and ignition is dev-only. The backend now sends unexpected exceptions (after shouldntReport filtering) through the already-installed posthog-php SDK via a reporter that can never throw and skips tests and keyless installs. The frontend enables posthog-js exception autocapture plus a Vue errorHandler for component errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgfZsao7eVj1Vo636nZQwS
The handlebars preview_template is tenant-authored markup, so its rendered output is now DOMPurify-sanitized before the v-html sink — escaping the interpolated values alone left stored XSS open to anyone with block-management rights. PreviewBridge previously accepted postMessage from any window; it now requires the message to come from the preview iframe itself with an origin matching a configured space environment, and addresses outgoing draft updates to the active environment's origin instead of *. Also carries the aria-labels for the preview toolbar buttons. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgfZsao7eVj1Vo636nZQwS
retry_after was 90s while migrations, backups and package builds run for many minutes, so the queue released still-running jobs to a second worker. retry_after now sits above the largest job timeout (invariant documented), the heavy jobs declare explicit timeouts and WithoutOverlapping locks with expiry, duplicate space migrations are rejected up front (409 in the controller, guard in the job), the base job gains backoff, and SetupConnection no longer serializes a resolved service into its payload. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgfZsao7eVj1Vo636nZQwS
- Restore the dashboard stats cache (keyed on include_activity so permission-gated activity can't leak between variants) - Stop echoing raw exception text from failed asset uploads; log the exception instead - Require min:8 on password change, matching register and reset - Refuse to persist SAML strict=false / unsigned assertions outside debug, and default both to true server-side - Eager-load block on the i18n relations in the delivery index (per-row loadMissing N+1) - Drop the dead TrimStrings middleware, its commented Kernel line and the stale slug.regex validation message Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgfZsao7eVj1Vo636nZQwS
Tables are hand-rolled on the ui/table primitives; nothing references the package. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgfZsao7eVj1Vo636nZQwS
Six types/*.d.ts files contained export statements, silently turning them into modules and un-globalizing every other interface they declare (~73 vue-tsc errors). They are pure ambient declarations again, and the now-redundant type imports are dropped across their consumers. The app-local useI18n() no longer launders the composer through any — t() is typed against the message schema — and vue-tsc drops from 229 to ~150 pre-existing errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgfZsao7eVj1Vo636nZQwS
141 icon-only buttons across the editor, content tree, asset manager, blocks admin, comments and settings now carry i18n'd aria-labels (reusing tooltip keys where they existed; 25 new keys in both locales). Upload dropzones, pickers and file cards that were bare div @click targets gained role, tabindex and enter/space handlers. Tree chevrons additionally expose aria-expanded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgfZsao7eVj1Vo636nZQwS
qrcode ships no types, so every import of it was implicitly `any`. prosemirror-model was installed twice — 1.25.11 at the root and a nested 1.25.4 under prosemirror-state — which made two structurally identical `Node` types mutually unassignable wherever tiptap's editor state met prosemirror-model's API. The override collapses them to one copy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0173rhDR9CndXH2LkWWkPFey
The account API-token table renders a "last used" column, but neither token resource exposed the field, so every row read "never" regardless of actual usage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0173rhDR9CndXH2LkWWkPFey
Preview.vue injected `content` into a binding that shadowed its own `content` prop; the two hold different things (saved content vs. the unsaved draft), so the injected one is now `injectedContent`. AssetItem and AssetFolder shadowed the imported `draggable` with their `draggable` prop, which is why `canDrag` read the prop while the call site read the import — the import is now aliased. TwoFactorSetupDialog passed a literal `maxlength="6"` after `:maxlength="6"`, shadowing the bound value with a string. TwoFactorQRCode asked toDataURL for `type: 'svg'`, which is not a canvas MIME type and silently fell back to PNG; the option is dropped rather than left as a false promise. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0173rhDR9CndXH2LkWWkPFey
lib/providers/ilum.ts is a `defineProvider` from `@nuxt/image/runtime`, a package this app does not depend on. It has been unreferenced since the Vue frontend replaced Nuxt, and docs/guides/nuxt.md points readers at the boilerplate repo's copy rather than this file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0173rhDR9CndXH2LkWWkPFey
SelectField and ComboboxField are generic SFCs, and useVModel's conditional prop typing degenerates to `T & false | T & true` under a type parameter. Both now use a plain writable computed and constrain their parameter to reka-ui's AcceptableValue, which is what the underlying Select/Combobox actually accept. settings-table is generic over its item type so callers keep concrete types instead of everything widening to TableItem. Text inputs accept a null model value and the field wrappers accept a null error, matching FormField and the many nullable API fields bound to them. DateTimeFormField also left min/max/placeholder in the props spread, so the trailing v-bind overwrote the explicitly bound, normalised values with the raw props. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0173rhDR9CndXH2LkWWkPFey
Several declarations had drifted from what the endpoints actually
return: BackupCreator described a `display_name` where the embedded
SimpleUserResource sends `name`/`avatar`, ContentResource omitted
`external_id`, SpaceSettings omitted `serial_gaps`, and the personal
access token responses had no declarations at all.
The space token endpoint answers create with `{ token, plain_text_token }`
rather than the usual `{ data }` envelope, so BaseResource takes a
CreateResponse parameter that defaults to the envelope.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0173rhDR9CndXH2LkWWkPFey
Mostly narrowing at the boundaries the compiler was already unhappy about: optional-chained `?.length > 0` guards that yield `number | undefined`, reka-ui handlers that receive AcceptableValue rather than the narrow type the callback declared, and nullable API fields bound to props that only accepted `string`. A few were substantive. ReleaseBadge, MigrationsTable and TeamRolesList passed `outline` as a badge *variant*, but it is a badge *type* that composes with a variant, so the compound-variant styling never applied. signup.vue called usePublicInviteQuery without the token it needs to resolve the invite. The tiptap TextClass and PlaceholderToken extensions now augment the Commands interface, which is what makes their chained commands visible to callers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0173rhDR9CndXH2LkWWkPFey
Adds a frontend job to the reusable test workflow, so it gates PRs, pushes to main, and — through the existing `uses:` in deploy.yml and release.yml — deploys and releases too. `bun run lint` runs oxlint with --fix, which rewrites files, and oxlint exits 0 on warnings regardless. The new lint:ci script drops --fix and passes --deny-warnings so a warning actually fails the build. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0173rhDR9CndXH2LkWWkPFey
Adds vitest (jsdom) with 5,015 tests across 161 files covering lib/, utils/, composables/, api/ and the leaf components, wires `bun run test` into CI, and fixes the findings the sweep turned up — recorded in tests/js/FINDINGS.md. The user-visible ones: content children were fetched unfiltered because nested query objects serialized to `[object Object]`; a successful content save could report "Failed to update content"; usePresence never enforced its reconnect cap and stacked timers; `await alert.confirm()` never settled on Escape; two parent-walks froze the tab on a cycle; deleting an unsaved wizard node erased saved descendants; ability checks answered from the previous space while the next one loaded; logout left the previous user's language and team behind; formatFileSize rendered the literal string "undefined". Also settled here: - clearClipboard only overwrites the system clipboard when it still holds our own item, so clearing a selection cannot destroy what the user copied elsewhere. - The wizard slug field re-seats itself on blur, so the normalized slug that will be saved is the one the user sees. - findItemById no longer reports an ancestor array's index for an object slot. - broadcastBlockOperation is guarded on isConnected like the field-update path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XguR2MwLwY1BZdgBKBC4SF
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XguR2MwLwY1BZdgBKBC4SF
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
This pull request introduces several improvements across the backend and CI pipeline, mainly focused on enhancing database transaction safety, error reporting, and frontend testing. The most significant change is the consistent use of per-model database connections for all content-related transactions, improving support for multi-database setups. Additionally, the CI pipeline now includes frontend linting and testing, and error reporting is enhanced with integration to PostHog.
Database Transaction Handling
$content->getConnection()->transaction(...)instead of the globalDB::transaction). This ensures correct transaction handling in multi-database environments and improves reliability. [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] [14]CI/CD and Frontend Testing
.github/workflows/tests.yml) now includes afrontendjob that runs linting, type-checking, and frontend tests using Bun, ensuring frontend code quality in addition to backend tests. [1] [2]Error Reporting and Logging
App\Services\PostHog\ExceptionReporterto the global exception handler, so unexpected errors are now reported to PostHog for observability. [1] [2]API and Defaults
CreateTokenaction now defaults new tokens to published-content read-only access unless explicit abilities are granted, improving security defaults.blockmodels for i18n relationships, reducing N+1 query issues.Configuration
.env.examplefor analytics integration.## SummaryLinked issue
How was this tested?
AI assistance
Checklist
./vendor/bin/pintrun on changed PHP filesbun run lintandbun run formatrun on changed TS/Vue files