Commit Graph

1069 Commits

Author SHA1 Message Date
emdashbot[bot] 4c3edadeec ci: release (#2419)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-18 13:26:49 +01:00
Armand Philippot eba44f7c63 i18n(fr): translate in French the 89 missing keys (#2464)
* i18n(fr): translate in French the 89 missing keys

* example.com

Co-authored-by: emdashbot[bot] <273199577+emdashbot[bot]@users.noreply.github.com>

---------

Co-authored-by: emdashbot[bot] <273199577+emdashbot[bot]@users.noreply.github.com>
Co-authored-by: Matt Kane <mkane@cloudflare.com>
2026-08-18 10:00:02 +01:00
helio-cf ef32567fd5 fix(core): generate row types for repeater fields (#2469)
* fix(core): generate row types for repeater fields

`fieldTypeToTypeScript` had no `repeater` case, so every repeater field
fell through to `default: return "unknown"` -- while the zod side derives
a precise row schema from the same `validation.subFields` metadata.

This was an oversight rather than a decision. The emitter's switch,
including its deliberate `case "json": return "unknown"`, dates from
43fcb9a1 and predates the field type: #111 added `repeater` without
touching this file at all, so neither the zod case nor the TS case
arrived with the feature. #2458 then added the zod case and left the TS
side as it was. Type generation is now the only consumer of `subFields`
that discards it -- media-usage extraction and the admin field editor
both read it.

Consumers cannot index or iterate `unknown`, so they declare row shapes
by hand and cast to them. Those casts silence the compiler: when a
sub-field is renamed or retyped, the generated type stays `unknown`, the
cast keeps asserting the old shape, and the drift surfaces as a silently
blank render rather than a type error.

- Build an inline row type from `subFields`, reusing the existing
  per-type mapping so a `select` sub-field enumerates its options and an
  `image` sub-field emits the same media literal as a top-level image.
- Type a sub-field that is not required as `T | null`, since
  `generateRepeaterRowSchema` applies `.nullish()` to it. A bare `T`
  would trade an honest `unknown` for an unsound type.
- Keep emitting `unknown` when `subFields` is absent or empty. A repeater
  with no declared rows is schema-valid, and `{}[]` would be falsely
  permissive.

`fieldTypeToTypeScript` now takes a structural subset of `Field` so a
sub-field can reuse the mapping without a type assertion.

Sub-field slugs match `/^[a-z][a-z0-9_]*$/`, so they are valid bare
identifiers and need no quoting in the emitted literal.

* fix(core): emit valid repeater row types from unvalidated sub-fields

Quote member names, keep the last declaration of a duplicated slug, and
fall back to `unknown` when `subFields` is not an array.

---------

Co-authored-by: Matt Kane <mkane@cloudflare.com>
2026-08-18 08:49:14 +00:00
emdashbot[bot] d328bdd6a6 chore: extract locale catalogs [skip ci] 2026-08-18 07:20:38 +00:00
Frank Bartolitsch f8a4fcefd2 fix(taxonomies): respect active locale in admin surfaces (#2342)
* fix(taxonomies): respect active locale in admin surfaces

Group translated definitions by logical identity before rendering admin navigation and editor choices. Scope visible term counts and cache entries to the resolved content locale.

* fix(taxonomies): preserve locale in admin term views

* ci: update query-count snapshots

* fix(admin): preserve locale sidebar active state

* fix(taxonomies): preserve exact locale updates

* test(perf): update locale query snapshots

---------

Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
2026-08-18 07:18:22 +00:00
emdashbot[bot] 75860bf32e chore: extract locale catalogs [skip ci] 2026-08-17 09:46:58 +00:00
Matt Kane 5c216c2f92 fix: preserve taxonomy assignments across locale fallbacks (#2508)
* fix(taxonomies): preserve assignments across locale fallback

* fix(admin): clarify taxonomy locale resolution

* perf(taxonomies): join locale fallbacks once

* fix(admin): gate taxonomy creation by permission

* chore: retry external preview build

* fix(admin): show flat taxonomy creation errors
2026-08-17 09:45:55 +00:00
emdashbot[bot] f15d433a4f chore: extract locale catalogs [skip ci] 2026-08-17 09:19:44 +00:00
Matt Kane 5289385499 fix: generate inline taxonomy slugs server-side (#2506)
* fix: keep inline taxonomy terms Unicode-safe

* fix: retry concurrent taxonomy slug collisions
2026-08-17 10:17:39 +01:00
emdashbot[bot] a5d31d547f chore: extract locale catalogs [skip ci] 2026-08-17 09:14:42 +00:00
Matt Kane 515c08d17c fix(scheduler): warn when scheduled maintenance stops (#2512)
* fix(scheduler): surface missed scheduled work

Persist maintenance heartbeats so authenticated editors are warned when overdue content indicates the scheduler has stopped.

* fix(scheduler): keep heartbeat writes non-fatal

A transient operator-health write must not reject maintenance that already completed successfully.

* test(scheduler): cover heartbeat upsert failures

Keep the non-fatal heartbeat contract exercised for both initial inserts and existing-row updates.
2026-08-17 10:12:12 +01:00
Matt Kane 40953d0859 feat(cli): diagnose broken Cloudflare scheduler wiring (#2511)
* feat(cli): detect broken scheduler wiring

Catch Cloudflare deployments that would silently miss scheduled work before they are deployed.

* fix(cli): keep doctor type-aware lint clean

* fix(cli): preserve additive doctor checks

Keep explicit database diagnosis intact, recognize Wrangler TOML, and avoid flagging Workers that do not opt into EmDash scheduled maintenance.

* fix(cli): make scheduler fixes config-aware

Keep doctor guidance copy-pasteable for Wrangler TOML users as well as JSON and JSONC sites.

* fix(cli): finish TOML doctor guidance

Keep every copy-pasteable scheduler fix and the command help accurate for all supported Wrangler formats.
2026-08-17 10:12:11 +01:00
emdashbot[bot] b87da7d12a chore: extract locale catalogs [skip ci] 2026-08-16 22:24:40 +00:00
Matt Kane 582823328e fix: preserve Unicode in routable content slugs (#2505)
* fix: keep routable content addressable across languages

* fix: keep invalid legacy slugs out of sitemaps

* fix: close routable publish bypasses

* fix: support slugless non-routable seeds

* fix: protect published routable slugs on update

* fix: normalize unusable slugs in seed exports

* fix: reject slugless routable schedules

* chore: clarify slug generation contract

* chore: remove editor implementation narrative

* test: keep revisionless fixture routable
2026-08-16 23:23:34 +01:00
emdashbot[bot] 140179959d chore: extract locale catalogs [skip ci] 2026-08-16 19:57:18 +00:00
Noah (Nguyen Pham) 318b821927 fix(admin): restore image and HTML toolbar actions (#2510)
* fix(admin): keep script formatting contextual

* fix(admin): restore image and HTML toolbar actions
2026-08-16 20:55:21 +01:00
Matt Kane ae87ce8772 Fix plugin updates being reverted on publish (#2494)
* fix plugin updates being reverted on publish

* test plugin drafts with valid portable text

* clarify draft-aware plugin update comment
2026-08-16 20:47:56 +01:00
Matt Kane ed1e79c0ab perf(core): batch public menu reference resolution (#2501)
* perf(core): avoid per-item menu reference queries

* refactor(core): keep menu lookup values minimal
2026-08-16 19:35:15 +01:00
Matt Kane 5394ecd098 fix(loader): expose folded bylines on live entries (#2500)
* fix(loader): make folded bylines available to live entries

* fix(loader): keep folded byline hydration in sync

* test(loader): assert the public byline credit type

* test(loader): pin folded custom field behavior

* test(loader): isolate byline field cache by dialect
2026-08-16 18:05:39 +00:00
Matt Kane 9b998224a5 fix(editor): protect inline edits from file navigation (#2502)
* fix(editor): protect inline edits from file navigation

* test(editor): rely on cancellable transfer behavior

* fix(editor): own unsupported file drag lifecycle

* fix(editor): reannounce repeated file guidance
2026-08-16 18:43:06 +01:00
Matt Kane cd4268d1d1 fix(security): prevent image parser denial of service (#2504)
* fix(security): prevent image parser denial of service

* test(security): cover all patched parser paths

* chore(build): preserve patched dependency rationale
2026-08-16 17:35:49 +00:00
Matt Kane 48806e224b fix(plugins): honor locale when creating content (#2498)
* fix(plugins): honor configured locale on content creation

* fix(plugins): align sandbox content locale types

* fix(workerd): apply default locale to batch creates

* docs(plugins): clarify explicit locale validation

* fix(plugins): initialize sandbox translation groups
2026-08-16 17:25:28 +00:00
emdashbot[bot] 1318b0cb32 chore: extract locale catalogs [skip ci] 2026-08-16 16:59:31 +00:00
Matt Kane 1c4d4f04b2 feat(core): expose outer user middleware hook (#2499)
* feat(core): wrap EmDash with user middleware

* fix(core): validate outer middleware config
2026-08-16 17:39:47 +01:00
Matt Kane 13db62c82f fix(core): match FileValue to persisted media snapshots (#2489)
* fix(core): match file values to persisted media snapshots

* fix(core): preserve sparse file snapshots safely
2026-08-16 17:02:11 +01:00
Matt Kane 2398b8d1bd fix: prevent unsupported Portable Text mark loss (#2493)
* fix: prevent unsupported Portable Text marks from being lost

* fix: keep editor open after rejected section insert
2026-08-16 16:59:22 +01:00
Matt Kane 964f51e5da Fix tag autocomplete navigation and overflow (#2491)
* fix(admin): make tag matches reachable

* test(admin): cover tag ranking and focus
2026-08-16 16:57:19 +01:00
Matt Kane 144e378161 fix(taxonomies): resolve omitted locales at write time (#2488)
* fix(taxonomies): avoid stale locale defaults

* ci: update query-count snapshots

* fix(perf): keep locale diagnostic off public cold path

* Revert "ci: update query-count snapshots"

This reverts commit 3dd19c561e889b60a88584a8c7863856118c1bc9.

* refactor(taxonomies): remove unreachable locale branches

---------

Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
2026-08-16 16:49:50 +01:00
emdashbot[bot] 38797a95c5 chore: extract locale catalogs [skip ci] 2026-08-16 12:14:49 +00:00
logelog 4c565ea7f9 feat(admin): allow trusted plugins to add content list columns (#2195)
* feat(admin): allow trusted plugins to add content list columns

* fix(admin): use ESM extensions for plugin columns

* test(admin): cover combined content list columns

* fix(admin): pass visible page to plugin columns

* fix(admin): use ASCII plugin column fallback

* fix(admin): enforce plugin column contracts

* docs(admin): use neutral column examples

* fix(admin): preserve plugin column state

* fix(admin): accept wrapped plugin column components

---------

Co-authored-by: logelog <194732487+logelog@users.noreply.github.com>
Co-authored-by: Matt Kane <mkane@cloudflare.com>
2026-08-16 12:13:37 +00:00
logelog e5cda04660 feat(content): filter indexed custom fields (#2213)
* feat(content): filter indexed custom fields

* fix(content): normalize field filters before validation

* test(content): cover indexed field filter contracts

* fix(content): cap indexed filter bind budget

* fix(content): preserve indexed filter query plans

* fix(content): keep the missing-collection error contract with field filters

* fix(content): settle both list queries before rethrowing

`findMany` races the page query against the count query. A collection whose
table is missing rejects both, and `Promise.all` returns on the first while
the other stays in flight holding a pooled connection. A Postgres pool
destroyed inside that window never finishes closing.

This predates the branch. The missing-collection regression test added here
is the first to reach the path on Postgres, so the suite cannot pass without
it.

* fix(content): resolve a missing collection before filter identifiers

Identifier validation ran first, so a request naming both a collection that
does not exist and an invalid filter field answered VALIDATION_ERROR while
the same request without filters answered COLLECTION_NOT_FOUND. The field
names reach the lookup as bound parameters, so deferring validation past the
collection check keeps the query parameterized.

Drop four comments that justify decisions rather than record an invariant.

* fix(content): resolve a missing collection before the filter cap

The cap on filter count threw before the collection lookup, so a request
naming a collection that does not exist alongside more than the allowed
number of filters answered VALIDATION_ERROR while the same request without
filters answered COLLECTION_NOT_FOUND. The cap stays where it is, since it
also bounds how many field slugs reach the lookup as bound parameters.

Drop the comment above the settled list queries. The missing-collection test
fails without them, which is where that contract belongs.

* refactor(content): drop the note above the filter cap

The tests around the cap already say what it protects.

---------

Co-authored-by: logelog <194732487+logelog@users.noreply.github.com>
Co-authored-by: Matt Kane <mkane@cloudflare.com>
2026-08-16 12:07:54 +00:00
Kevin Kyburz 8313255a60 feat(plugins): expose authenticated caller to route handlers as ctx.user (#1947)
* feat(plugins): expose authenticated caller to route handlers as ctx.user

The plugin API catch-all resolves and authorizes the caller, then drops
it before dispatch, leaving plugins no safe way to know who is calling
a private route. Thread the validated user through handlePluginApiRoute
into RouteContext (native format) and routeCtx.user (standard format,
in-process and worker sandboxes alike) as the read-only UserInfo shape.

Public routes and machine tokens with no bound user receive undefined;
the catch-all only forwards the caller after private-route auth, so an
ambient admin session is never bound to a public route.

Fixes #812

* docs(core): use canonical users capability name

* fix(core): forward callers to MCP plugin routes

---------

Co-authored-by: Matt Kane <mkane@cloudflare.com>
2026-08-16 12:36:43 +01:00
Mason James a9ace36a0d feat(mcp): add safe schema update tools (#2354)
* feat(mcp): add safe schema update tools

* fix(mcp): harden schema update safety

* fix(schema): preserve partial update invariants

* fix(schema): skip unchanged field indexes

* fix(mcp): expose indexed field updates

* fix(schema): preserve collection update behavior

* fix(mcp): align schema update validation

* fix(core): share URL pattern cache state

* fix(mcp): reuse collection support schema

---------

Co-authored-by: Matt Kane <mkane@cloudflare.com>
2026-08-16 10:21:41 +01:00
emdashbot[bot] 19978ffcfc chore: extract locale catalogs [skip ci] 2026-08-16 08:19:52 +00:00
Eyüp Can Akman 77080152db fix(admin): parse API token created date as UTC (#2467)
Co-authored-by: Matt Kane <mkane@cloudflare.com>
2026-08-16 09:18:45 +01:00
emdashbot[bot] 0991de0e09 chore: extract locale catalogs [skip ci] 2026-08-16 06:42:11 +00:00
CacheMeOwside 5224b5711a feat(core): configurable titleField and dateField per collection (#1973)
* feat(core): configurable displayField and dateField per collection

* style: format

* ci: update query-count snapshots

* add fixes as per PR comments

* ci: update query-count snapshots

* reject field type changes that break displayField/dateField

* rename displayField to titleField

* match content-list search and suggestions against configured titleField

* ci: update query-count snapshots

* update integration tests

* style: format

* removed reference to PR from comments

* Remove PR reference

Co-authored-by: emdashbot[bot] <273199577+emdashbot[bot]@users.noreply.github.com>

---------

Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
Co-authored-by: emdashbot[bot] <273199577+emdashbot[bot]@users.noreply.github.com>
2026-08-16 07:41:13 +01:00
Daniel 4e76317978 fix(core): remove stale restampEntryPivot call that crashes content publish (#2466)
* fix(core): remove stale restampEntryPivot call that crashes content publish

The merge of #2460 and #2461 left main with a publish path that calls
restampEntryPivot, a method #2461 deleted together with the pivot
denormalization it maintained. Every content publish now throws
"this.restampEntryPivot is not a function".

No replacement call is needed: since migration 068 the pivot columns the
call re-stamped are legacy, and reads join the authoritative ec_* row.

* chore: add changeset for the publish crash fix

The fix changes the published emdash package, and release notes are
built from changeset files, so the crash fix needs an entry even though
the crash itself never shipped in a release.
2026-08-14 16:04:43 +01:00
Matt Kane 6602ae05bc Fix taxonomy filter detection across collections (#2459)
* fix(loader): scope taxonomy filters to collections

* ci: update query-count snapshots

* fix(loader): harden taxonomy name cache

---------

Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
2026-08-13 17:23:31 +00:00
Matt Kane 598e6fb95f Fix taxonomy assignments across content translations (#2461)
* fix: share taxonomy assignments across translations

* fix: count shared taxonomy assignments once

* ci: update query-count snapshots

---------

Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
2026-08-13 18:20:40 +01:00
Matt Kane d236776195 fix: prevent revisionless publish data loss (#2460) 2026-08-13 16:43:41 +00:00
Matt Kane d7fe781ed9 fix: prevent queued pruning from deleting concurrent revisions (#2457) 2026-08-13 16:02:18 +00:00
Matt Kane 7385d434ca fix: enforce repeater constraints on content saves (#2458) 2026-08-13 16:00:29 +00:00
Kevin Kyburz 1d3111cceb fix(cloudflare): fall back to the direct D1 binding when session queries hang (#1716)
* fix(cloudflare): fall back to the direct D1 binding when session queries hang

* fix(cloudflare): broken fast path without re-racing, globalThis singleton for the session guard

* style: oxfmt pass on the session-guard test

* style: move lint suppressions onto the exact assertion lines in raw()

* docs: update stale JSDoc — guard singleton lives on globalThis

---------

Co-authored-by: Matt Kane <mkane@cloudflare.com>
2026-08-13 15:46:09 +00:00
Eyüp Can Akman 592a0e37c6 fix(admin): parse SQLite timestamps as UTC (#1121)
* fix(admin): parse SQLite timestamps as UTC

SQLite datetime('now') stores timestamps with no timezone suffix, which new Date() reads as local time.
Revision and dashboard timestamps in the admin UI drifted by the viewer's offset.

Closes #919

* fix(admin): route content list and editor dates through parseTimestamp

* fix(admin): route the scheduled publish date through parseTimestamp

---------

Co-authored-by: Matt Kane <mkane@cloudflare.com>
2026-08-13 15:33:30 +00:00
Dipak Chaudhari 6d9a0d54c0 fix(core): trusted standard-format route handlers get the public single-arg RouteContext (#2136)
* fix(core): call standard-format route handlers with the public single-arg RouteContext

adaptSandboxEntry always invoked route handlers with the sandboxed
two-arg (routeCtx, pluginCtx) convention. A standard-format plugin
authored against the exported single-arg RouteContext API silently got
routeCtx as its only argument (JS drops the extra parameter), so
ctx.storage / ctx.email / ctx.kv and every other capability surface
were undefined and any route touching them threw at runtime.

The definition's id is the format signal — definePlugin requires it and
sandbox-format default exports never carry one (identity comes from the
manifest), the same rule define-plugin.ts documents. Standard-format
handlers now receive the incoming RouteContext unchanged; sandbox-format
entries keep the two-arg flattened convention.

Closes #2079

* style: format

* fix(core): use 'in' narrowing for the standard-format id probe

The id check cast `definition` to `{ id?: unknown }`, which trips
oxlint's no-unsafe-type-assertion under --deny-warnings and failed Lint.
Use the `in` operator instead — it narrows without an assertion and
reads the same.

* style: format

---------

Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
Co-authored-by: Matt Kane <mkane@cloudflare.com>
2026-08-13 15:51:56 +01:00
emdashbot[bot] ced6fcac0b chore: extract locale catalogs [skip ci] 2026-08-13 13:38:22 +00:00
logelog fefb702763 feat(content): support indexed custom field sorting (#2212)
* feat(content): support indexed custom field sorting

* fix(content): remove redundant indexed cursor cast

* fix(content): use generated custom field indexes

* fix(content): keep indexed cursor scans ordered

* test(content): capture qualified list queries

* fix(admin): clear the indexed flag for non-indexable field types

* test(admin): cover clearing the indexed flag on type change

* refactor: drop comments that address the reviewer

Three blocks defend sending `indexed: false` or narrate a Playwright
workaround. The tests already name the contract they cover. The bind-budget
note above the seed batch size stays: it records a limit a reader would
otherwise raise.

* test(core): cover indexed ordering on PostgreSQL

* refactor(schema): simplify indexable field comment

* fix(test): import the indexed migration from its current slot

The file moved to 061 when upstream took 059; this import still named 060,
so the suite could not load the module at all.

* fix(seed): reject unsupported indexed field types

* test(core): allow PostgreSQL ordering setup

* fix(content): preserve indexed field contracts

* fix(test): import indexed migration from slot 066

* fix(core): keep localized custom ordering seekable

---------

Co-authored-by: logelog <194732487+logelog@users.noreply.github.com>
Co-authored-by: Matt Kane <mkane@cloudflare.com>
2026-08-13 14:34:36 +01:00
marcusbellamyshaw-cell 4831f77454 fix: exclude image/svg+xml from default media upload allowlist (#2250)
* fix: exclude image/svg+xml from default media upload allowlist

GLOBAL_UPLOAD_ALLOWLIST used a bare "image/" prefix match, which
unintentionally included image/svg+xml. There is no upload-time content
validation for SVG, so a Contributor+ user could upload an SVG with an
embedded <script>. Enumerate the safe raster types explicitly instead.
Field-specific allowedMimeTypes overrides are unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(admin): match Images upload preset to the raster-only allowlist

The Images preset in AllowedTypesEditor still stored a bare "image/"
prefix, which prefix-matches image/svg+xml. Since upload routes widen
the global allowlist to a field's own allowedMimeTypes, picking this
preset re-admits SVG uploads and undermines the new default that
excludes them.

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Matt Kane <mkane@cloudflare.com>
2026-08-13 14:26:39 +01:00
emdashbot[bot] f9e44628af chore: extract locale catalogs [skip ci] 2026-08-13 13:25:07 +00:00