Skip to content

Enhance backend stability, security, and type safety - #19

Open
badmike wants to merge 21 commits into
mainfrom
feature/typecheck-lint-ci
Open

Enhance backend stability, security, and type safety#19
badmike wants to merge 21 commits into
mainfrom
feature/typecheck-lint-ci

Conversation

@badmike

@badmike badmike commented Jul 31, 2026

Copy link
Copy Markdown
Member

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

  • All content-related actions now use the model’s own database connection for transactions (e.g., $content->getConnection()->transaction(...) instead of the global DB::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

  • The CI workflow (.github/workflows/tests.yml) now includes a frontend job 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

  • Integrated PostHog exception reporting by adding App\Services\PostHog\ExceptionReporter to the global exception handler, so unexpected errors are now reported to PostHog for observability. [1] [2]
  • Improved asset storage error logging by logging the full exception object instead of just the message, and returning a generic error message to the client.

API and Defaults

  • The CreateToken action now defaults new tokens to published-content read-only access unless explicit abilities are granted, improving security defaults.
  • The content API now eager-loads related block models for i18n relationships, reducing N+1 query issues.

Configuration

  • Added PostHog API key and host configuration to .env.example for analytics integration.## Summary

Linked issue

How was this tested?

AI assistance

Checklist

  • Commits follow the Gitmoji format
  • ./vendor/bin/pint run on changed PHP files
  • bun run lint and bun run format run on changed TS/Vue files
  • Tests pass locally (and new behavior is covered where it makes sense)
  • AI usage is disclosed above (or not applicable)
  • I understand and can explain the code I'm submitting

badmike and others added 21 commits July 29, 2026 20:14
\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
@badmike badmike self-assigned this Jul 31, 2026
@badmike
badmike requested a review from Copilot July 31, 2026 09:20

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​types/​qrcode@​1.5.61001007481100
Addednpm/​@​vitest/​coverage-v8@​4.1.10991007999100
Addednpm/​vitest@​4.1.10981007999100
Addednpm/​jsdom@​30.0.18510010096100
Addednpm/​@​vue/​test-utils@​2.4.11991009388100

View full report

@socket-security

Copy link
Copy Markdown

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.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm data-urls is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?npm/jsdom@30.0.1npm/data-urls@7.0.0

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/data-urls@7.0.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm happy-dom is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?npm/vitest@4.1.10npm/happy-dom@20.11.1

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/happy-dom@20.11.1. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm js-beautify is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?npm/@vue/test-utils@2.4.11npm/js-beautify@1.15.4

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/js-beautify@1.15.4. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm jsdom is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: package.jsonnpm/jsdom@30.0.1

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/jsdom@30.0.1. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants