* fix: Allow service worker to load on custom domains
Add explicit worker-src 'self' so the service worker can register on
team custom domains. Without it, browsers fall back to script-src which
only lists env.URL and env.CDN_URL, blocking /static/sw.js on hosts
like docs.getoutline.com.
* fix: Switch worker-src approach to script-src 'self' for type safety
The @types/koa-helmet definitions don't include workerSrc. Add 'self'
to script-src instead — worker-src falls back to script-src per spec,
and 'self' matches the document origin on custom domains.
* fix: Properly add worker-src directive without script-src widening
Extract the CSP directives to a local variable so workerSrc can be
included despite koa-helmet's outdated type definitions missing it
(the underlying helmet supports it). Also drop @types/koa-helmet
since the package now ships its own (equivalent) types.
When /oauthClients.info returns an AuthorizationError, ApiClient logs
the user out and clears auth.team. The subsequent re-render of the
Authorize component hit the strict useCurrentTeam() and threw before
the error UI could render. Make the inner hook tolerate a missing team
and fold it into the existing error branch.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* fix: Normalize IP addresses to avoid validation errors on audit columns
Koa's `ctx.request.ip` can yield values that fail Sequelize's `isIP`
validation (X-Forwarded-For chains, IPv6 zone identifiers, "unknown"
from misconfigured proxies). This drops the IP metadata silently
instead of raising a 500 on Event/User writes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: Cover IP normalization on User setters
Reviewer feedback. Also switches the column-options `set` to TypeScript
get/set accessors — the original approach was shadowed by the class
field declaration and never actually fired, which the new tests would
have caught.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
The onerror handlers in FileHelper passed the raw DOM Event to reject,
which Sentry surfaced as "Event captured as promise rejection" with no
stack. Reject with an Error and revoke the blob URL on failure.
* fix: Allow reordering subdocuments with document-only access
When a user has "Manage" (or any move-eligible) permission on a parent
document but no access to its collection, the sidebar drop cursors were
hidden because they gated on collection.isManualSort, and the move
handler bailed out because it built the payload from collection.id.
Fall back to the document's own collectionId and the move policy so the
reorder UX works for sourced document memberships.
* fix: Structure not refetched
parentDocumentId not provided
* refactor: introduce declarative menu registry for selection toolbar
Replace the hard-coded if-else chain in SelectionToolbar with a
priority-based menu registry system. Extensions can now declare
selection toolbar menus via `selectionToolbarMenus()`, following the
same pattern as `commands()` and `keys()`.
Key changes:
- Add SelectionContext interface computed once per toolbar render
- Add SelectionToolbarMenuDescriptor for declarative menu registration
- Add selectionToolbarMenus() to Extension base class
- Add buildSelectionContext() utility to eliminate repeated state queries
- ExtensionManager collects and sorts menus from all extensions
- SelectionToolbarExtension registers all 10 existing menus
- All menu functions now accept SelectionContext instead of raw state
- SelectionToolbar uses registry lookup instead of if-else chain
https://claude.ai/code/session_01MRyFysrGM9d8NhbAs7nrtU
* refactor: import t directly from i18next in menu functions
Remove the `t: TFunction` parameter from all menu functions and the
`SelectionToolbarMenuDescriptor.getItems` signature. Each menu file
now imports `t` directly from i18next, matching the pattern used
throughout the rest of the codebase (e.g. Image.tsx, Link.tsx).
https://claude.ai/code/session_01MRyFysrGM9d8NhbAs7nrtU
* refactor: move divider menu into HorizontalRule node extension
The divider selection toolbar menu is now declared via
selectionToolbarMenus() on the HorizontalRule node class, co-locating
the menu with the node that owns it. Delete the standalone
app/editor/menus/divider.tsx file and remove the entry from
SelectionToolbarExtension.
This is the first menu migrated from the centralized toolbar extension
to an individual node extension, demonstrating the pattern for the
remaining menus.
https://claude.ai/code/session_01MRyFysrGM9d8NhbAs7nrtU
* refactor: check readOnly in matches predicate for divider menu
https://claude.ai/code/session_01MRyFysrGM9d8NhbAs7nrtU
---------
Co-authored-by: Claude <noreply@anthropic.com>
The MutationObserver callback could throw an uncaught RangeError when
posAtDOM returned a position outside the document, since the existing
try/catch only wrapped the observer setup, not the async callback.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
The Notion API can return transient 5xx errors during imports. Retry these
up to 8 times with exponential backoff, tracked separately from the existing
timeout/rate-limit retry budget.
When a user query produces a pg-tsquery output ending in mixed `&` and `\`
characters (e.g. `"plugins"&\`), stripping them with separate single-char
regexes in a fixed order could leave a dangling `&` operator, causing
Postgres to reject the query with "no operand in tsquery".
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Attachments whose key ends in a trailing slash have no filename
component and cause yazl to throw, aborting the entire export. Skip
them with a warning and continue the export instead.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Guard against null collaboratorIds when persisting collaborative
updates; the DB column has no default and can be NULL on older rows.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* perf: Add missing indexes on foreign keys referencing documents
Cascade deletes on the documents table were forced into sequential scans
on file_operations, share_subscriptions, and access_requests because
their documentId columns lacked a usable single-column index.
Also fixes lint-staged to skip oxlint when every staged file matches an
.oxlintrc.json ignore pattern, since oxlint exits 1 in that case.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Handle oxlint no-files exit instead of mirroring ignorePatterns
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* fix: New Danish translations from Crowdin [ci skip]
* fix: New Danish translations from Crowdin [ci skip]
* fix: New Catalan translations from Crowdin [ci skip]
* fix: New Catalan translations from Crowdin [ci skip]
* fix: New Catalan translations from Crowdin [ci skip]
* fix: New Catalan translations from Crowdin [ci skip]
* fix: New Catalan translations from Crowdin [ci skip]
* fix: New Catalan translations from Crowdin [ci skip]
* fix: New Japanese translations from Crowdin [ci skip]
* fix: New Catalan translations from Crowdin [ci skip]
* fix: New Portuguese, Brazilian translations from Crowdin [ci skip]
* fix: New Catalan translations from Crowdin [ci skip]
* fix: New Catalan translations from Crowdin [ci skip]
* fix: New Catalan translations from Crowdin [ci skip]
* fix: New Catalan translations from Crowdin [ci skip]
* fix: New Catalan translations from Crowdin [ci skip]
* fix: New Spanish translations from Crowdin [ci skip]
* chore: Update JSON importer to use zip streaming, new importer flow
* chore: Drop teamId from import urlId collision check and remove unused internal-id scaffolding
urlId is globally unique on Document/Collection so the team scope was wrong.
Also removes leftover internal-id generation in JSONAPIImportTask that was
never used in task input/output.
* Restore classes used upstream
* fix: documents.list with Draft status filter throws database error
The count() query referenced $memberships.id$ in WHERE but had no
membership include, causing "missing FROM-clause entry for table
memberships". The findAll path was also silently dropping drafts because
withMembershipScope defaulted to defaultScope (which filters publishedAt
!= null). Pre-fetch the user's UserMembership document IDs and filter by
id IN (...) on both find and count, and pass includeDrafts: true when
the Draft filter is active.
* Preserve template/trial filters when including drafts
* Move template/trial filters into withDrafts scope
* Revert withDrafts scope filters, apply at call site instead
Adding template/trial filters to withDrafts broke includes in places
like Share's withCollectionPermissions where the document include must
remain optional (LEFT JOIN) — adding a where promoted it to INNER JOIN
and dropped shares without a documentId.