* 🐛 Fix Japanese IME Enter duplication in comment input
Comment keydown handler treated every Enter as a Penpot
line-break action, so confirming an IME composition
duplicated the text with an extra newline and a
zero-width space. Guard the whole custom keydown
processing while the event belongs to an active IME
composition, mirroring the v3 text-editor precedent.
Closes#11757
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Keep composing Escape from closing comment thread
The parent floating-thread keydown handler closed the
thread on every Escape, including one that cancels an
active IME composition. Apply the same composition
guard so composing Escape stays owned by the IME while
plain Escape still closes the thread.
Closes#11757
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Test comment IME guard through key-action resolver
The composition predicate test only verified the
predicate itself, so a guard moved to the wrong place
or a handler bypassing it would stay green. Resolve
comment and thread keydowns through a pure
resolve-comment-key-action seam and verify the
observable behavior: composing keys yield :ime-owned
with zero Penpot side effects while the same plain
keys keep their existing commands.
Closes#11757
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Test comment IME handlers through direct calls
The key-action resolver only verified a return value,
so handler wiring regressions would stay green, and it
read the mention snapshot before handle-select ran,
changing the existing ordering. Remove the resolver,
extract the two handler bodies as directly callable
fns with the original select-first ordering, and
assert the fired side effects instead.
Closes#11757
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>
AI-assisted-by: muse-spark-1.3-contributor
* 📚 Remove text-editor v3 references from comment IME docs
The comment IME guard is specific to the comment editor, so the
docstrings no longer present it as following a v3 text-editor or
render-engine precedent. Reviewers read that wording as tying this
comment bug fix to unrelated subsystems.
Only docstring text changes; handler logic and test assertions are
untouched.
Closes#11757
AI-assisted-by: deepseek-v4.1-flash
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
* 🐛 Review comments, and fix edge case
---------
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
* ⬆️ Upgrade MCP SDK to v2 and remove HTTP sessions
MCP's per-request protocol removes the need to retain HTTP sessions.
Use the v2 handler to manage each request's transport and lifecycle,
so requests can reach any server instance without session affinity
or the workaround that adopts sessions through private SDK fields.
Keep legacy SSE support and the shared plugin and Redis bridges.
Remove the shared expiry checker, including legacy SSE idle expiry;
SSE connections now remain until disconnection or server shutdown.
Verify stateless requests, token isolation, and legacy compatibility.
Resolves#11827
AI-assisted-by: gpt-6-astra
* 🔥 Remove legacy MCP SSE support
Use Streamable HTTP as the sole MCP client transport so the server no
longer needs a separate SSE connection registry or lifecycle.
Remove /sse and /messages, their nginx routes, and the server-legacy
dependency. Legacy SSE clients must switch to /mcp; older Streamable
HTTP clients remain supported. Document the migration and verify that
the removed endpoints return 404.
Resolves#11846
AI-assisted-by: gpt-6-astra
* 🐛 Fix error when reset overrides on a swapped copy
* 🐛 Add regression test for reset overrides inside group
Cover the case where a nested copy lives inside a group (not directly
under the instance root). After a swap, reset overrides must undo the
swap without error (#11656).
---------
Co-authored-by: Alejandro Alonso <alejandroalonsofernandez@gmail.com>
A compose port mapping delivers traffic to the container address,
never to loopback, so `listen 127.0.0.1:8082` made
`ports: <host>:8082` fail from the host. Both configs (image
template and devenv) now use `listen 8082`, which binds every
interface and matches the implicit bind of the public
`listen 8080 default_server`.
Rewrite both block comments to state the new bind and who decides
access from outside the host. The scrape URI stays on
127.0.0.1:8082: it still reaches the socket.
AI-assisted-by: mimo-v2.6-flash-free
* ✨ Send notification email on password change
Add a password-changed email template and send it after every
successful password change or first password set operation. This
gives users a visible signal when a new authentication factor is
established on their account.
Closes#11392
AI-assisted-by: longcat-2.0
* 🐛 Address code review feedback on password notification
- Send password-changed notification from recover-profile too
(forgot-password reset path was missing the email).
- Strengthen test assertions to verify email factory, recipient,
and name via :call-args-list instead of just call-count.
- Add negative test: no email sent when old-password is wrong.
- Wrap pre-existing update-profile-password test with send! mock
to keep its scope focused.
Ref: PR #11393
AI-assisted-by: longcat-2.0
Binfile import ran the whole import in a single transaction with
idle_in_transaction_session_timeout disabled (= 0), so a stalled
import could retain a connection pool slot indefinitely. Set a
finite 20 minutes ceiling via SET LOCAL instead (a compile-time
constant interpolated into the SQL; PostgreSQL does not accept bind
parameters on SET).
The v3 importer also located each file data by rescanning the full
zip entry collection once per manifest file and once per page,
making the cost close to quadratic on large files. Replace the
per-file regex matchers with a single classification pass that
groups entries by their raw path shape; consumers now lookup their
entries per file and page. As a deliberate tightening, the .json
suffix is matched literally: the previous regexes left the dot
unescaped, so crafted paths like files/<f>/tokensXjson or
objects/x-json matched by accident and are now ignored.
Closes#11579
AI-assisted-by: omen-alpha
* 🐳 Add upstream diagnostics to nginx access log
Enrich every access-log line with the internal journey of the request:
the status the backend answered (us), the time spent connecting to it
(uct), the time spent waiting for its answer (urt) and the internal
address that served the request (ua).
A plain 502 line used to say nothing about where the request died. With
this format, the tail of the line classifies the failure: connection
rejected, backend accepted and hung (uct + urt under 1s), or backend
stuck until read timeout. This was the missing witness in the Sep 20
incident, where nginx received connection resets with zero timeouts and
zero rejections.
Applied both to the production image template and the devenv config.
With proxy_pass on variables there is no upstream keepalive, so uct
measures one real TCP connection per request.
Parsing the new fields (us, uct, urt, ua) on the log shipper is left to
ops, so they can be filtered in Loki.
AI-assisted-by: glm-5.3-flash
* 🐳 Add stub_status endpoint for nginx metrics
Add a dedicated localhost-only server (listen 127.0.0.1:8082) exposing
/stub_status next to every other location of the public server. Ops can
run the official nginx-prometheus-exporter as a sidecar against
http://127.0.0.1:8082/stub_status and get nginx_connections_active,
accepted vs handled, reading/writing/waiting and request rates in
Prometheus.
Binding it to localhost and its own server keeps it unreachable from
outside the host and out of the public surface, and access_log off
avoids polluting Loki with one line per Prometheus scrape. The base
image already ships stub_status compiled in, so no image rebuild is
needed.
Applied both to the production image template and the devenv config.
AI-assisted-by: glm-5.3-flash
* ✨ Expose http server gate metrics (worker and connector)
The backend already measured dispatch latency but nothing reported the
state of the "house door": the xnio worker queue and threads, and the
monitor-level listener counters. This was the exact blind spot of the
Sep 20 incident, where the server kept answering health checks while it
accepted connections and dropped them without response.
Add a periodic metrics sampler that lives and dies with the http
server (single daemon thread, 15s interval, each sample guarded so an
unexpected error does not cancel subsequent runs) and publishes:
- worker (xnio MXBean gauges): penpot_http_worker_queue_size,
busy_threads, pool_size and max_pool_size. Negative samples are
discarded: the MXBean transiently reports -1 on the busy thread
count (verified live), and a stale negative would read as zero.
- listener (Undertow connector statistics, enabled via the new
:server/statistics yetti option): penpot_http_connector_active*
_connections gauge and requests_total / errors_total counters.
Undertow exposes absolute totals, so the sampler keeps a watermark
atom and publishes deltas, skipping (and moving forward past) a
counter reset.
The connector-level part depends on yetti v11.11, which now accepts
a :server/statistics server option (patch authored and released
upstream; before it, ListenerInfo#getConnectorStatistics always
returned nil).
New tests cover the samplers with fake MXBean/collector statistics
against real prometheus collectors, including the negative-sample
filter, the delta/watermark logic and the sampler lifecycle.
AI-assisted-by: glm-5.3-flash
* 🐛 Include jdk.management in the backend runtime JRE
The production image builds a trimmed JRE with jlink and omitted
jdk.management. Without that module the OS MXBean is
sun.management.BaseOperatingSystemImpl, which has no
getProcessCpuTime, getOpenFileDescriptorCount nor
getMaxFileDescriptorCount. The prometheus client StandardExports
reads those getters reflectively and collect() swallows the
NoSuchMethodException, so process_open_fds, process_max_fds and
process_cpu_seconds_total silently disappeared from /metrics while
the other process_* families kept flowing.
Verified against Prometheus: the app job only ever exposed
process_start_time_seconds, process_virtual_memory_bytes and
process_resident_memory_bytes; the fd and cpu families were absent.
Reproduced locally by running the backend metrics registry on a JRE
built with the same jlink module list (false/false/false) and on one
with jdk.management added (true/true/true).
Add the module to --add-modules and pin the metric contract with
backend-tests.metrics-test.
AI-assisted-by: deepseek-v4.1-flash
* ♻️ Build the http metrics sampler on promesa.exec
Replace the hand-rolled ScheduledThreadPoolExecutor and ThreadFactory
with promesa.exec primitives: px/scheduled-executor with a daemon
thread factory, and a px/schedule chain that reschedules the next
sample when the current one finishes.
Beyond fitting the existing periodic-task pattern (worker/cron,
rpc/rlimit), the chained schedule makes the docstring promise real:
with scheduleAtFixedRate an exception escaping the runnable cancelled
the following executions, while the reschedule now happens in a
finally block.
The sampler shutdown uses px/shutdown-now (shutdown! is deprecated in
promesa 12.0.0) to cancel the pending sample, keeping the previous
halt semantics.
The lifecycle test moves to the promesa predicates and a new test
covers the error-resilience promise: the first sample runs, throws,
and the next one is still scheduled.
AI-assisted-by: deepseek-v4.1-flash
* ♻️ Tighten the http metrics samplers
The samplers are leaf functions: they receive what they need and
publish it. Drop the internal nil guards (if there is no metrics
instance or no mxbean there is nothing to call them for) and move the
checks to the boundary, where the optional data is resolved:
sample-http-metrics now short-circuits with some-> and when-let.
Write the four worker gauges as four static operations instead of a
vector of pairs walked by doseq: the set is fixed, so the collection
only adds an allocation and hides each operation.
Drop the ! suffix from the sample-*-metrics family: ! marks a function
whose contract is to mutate state, while these report, and the mutation
happens in the mtx/run! they call. The constant true return, which only
existed so the removed guard tests could assert it, goes away too.
Tests follow the move: the internal-guard tests are replaced by one
boundary test (a nil server publishes nothing).
AI-assisted-by: deepseek-v4.1-flash
* 📚 Add the function design rules memory
Document the rules that came out of the http metrics sampler review:
preconditions are checked at the boundary instead of re-checked in the
core, optional-by-design data is guarded where the optionality is born,
a fixed set of operations is written statically, ! marks mutation and
not reporting, and production code is not shaped for tests.
Also state in the memory maintenance guide that memories must not use
manual line wrapping.
Linked from critical-info so it is read when designing a solution or
an API, not only when touching the samplers.
AI-assisted-by: deepseek-v4.1-flash
* 📚 Unwrap the critical-info memory lines
The memory maintenance guide forbids manual line wrapping, so rewrite
critical-info with one line per bullet and paragraph. A stray `*` at
the start of one continuation line is dropped.
AI-assisted-by: deepseek-v4.1-flash
* ♻️ Drop the redundant guard in the http server halt
create-metrics-sampler always returns the scheduler, so the sampler is
always present when integrant calls halt-key!; the nil check was dead
code, same as the yt/stop! call next to it.
AI-assisted-by: deepseek-v4.1-flash
* ✨ Add srepl helper to delete profiles by email
Add `delete-profiles-by-email!` to app.srepl.main. It accepts a
single email, a comma separated list of emails or a coll of emails,
resolves each profile, logs it to audit and enqueues the
delete-object task. The deleted-at is backdated with the configured
deletion-delay so profiles and their owned teams are purged on the
next gc pass.
Extract the per-email deletion logic into a private fn and reuse it
from `delete-profiles-in-bulk!`. Add tests for the new
`parse-emails` helper.
AI-assisted-by: glm-5.3-flash
A masked group now closes its focus scope on the mask pass, pops the
nested stacks on the content pass only, and draws its exit strokes on
the content pass. The focus depth also resets when each tile's walk
starts.
Closes#11805
AI-assisted-by: claude-opus-5
* ✨ Add account lockout after failed login attempts
Implement per-account brute-force protection using a Redis-backed
failed-login counter. After 5 failed attempts within 15 minutes, the
account is temporarily locked out and all login attempts (including
with the correct password) are rejected with a 429 response.
Closes#11397
AI-assisted-by: longcat-2.0
* 🐛 Bind LDAP session to directory-verified profile
The account-lockout change added a shortcut that preferred the
profile matching the typed email over the one returned by the LDAP
directory. These can differ with aliases, UPNs, or multi-valued mail
attributes, letting a user with valid LDAP credentials bind a session
to another Penpot account.
Keep the typed-email profile only for lockout checks. After LDAP
succeeds, resolve the session profile from the directory identity as
before and clear failed attempts on that profile.
AI-assisted-by: deepseek-v4.1-flash
Imported token sets were collapsed, hiding what had just been imported.
Fixes#9819
Signed-off-by: Akshit Nassa <akshitnassa412@gmail.com>
Co-authored-by: Akshit Nassa <akshitnassa412@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
Classify save failures as transient or terminal (`transient-error?`
over the repo retryable types plus `:invalid-save-response`).
Transient failures keep the head commit queued under a new `:retrying`
status and resend it with backoff (2s/8s/20s, then terminal):
stamp rotation reuses the same `:commit-id`, the in-flight guard
prevents double-sends, and episode tokens silence stale timers.
One tagged reconnect notice per episode (hidden on save and on
terminal failure, silent recovery) plus a `:retrying` save-indicator
state; the browser `online` event and new edits resume the episode.
Terminal failures keep the exact `:error` path. Covers tasks 4, 6
and 7 with 31 persistence tests; updates the persistence memory.
Relates to #11724
AI-assisted-by: muse-spark-1.3-contributor
Connectivity and gateway failures (network, offline, 502/503 and
nitrate configuration) are not application defects, but offline fell
through to :default and 502/503 rendered exception-page, so they
reached the internal error reports and alerts with the full payload
(stack plus the last events). They are now classified as environment
failures and reported as audit-only handled-exception events.
generate-report accepts an explicit :format, as keyword arguments or as
a trailing map. :compact keeps the context header plus type, code and
uri, and skips the stack, the ex-data dump (which may contain request
headers) and the last-events list. flash derives the payload format from
the cause, so environment failures get a compact report; the audit event
name stays the canonical one requested by the caller
(handled-exception/unhandled-exception) because external tooling filters
on those names. Environment fingerprints drop the stack frame, so
grouping does not depend on the internal call site.
submit-report now requires an exception cause: a report without one is
ignored instead of using a separate fallback fingerprint, so a single
fingerprint format governs every report.
:offline gets its own handler and both connectivity handlers show the
new errors.connection-error message instead of the generic toast.
Closes#11743
AI-assisted-by: deepseek-v4.1-flash
Add a report governor in app.main.errors: each report carries
a fingerprint, the first occurrence is always emitted, and
repeats within 2 minutes are counted and included in the next
emitted report as :occurrences. The fingerprint cache is
bounded by evicting the oldest entry.
flash reserves the report before generating it, so suppressed
occurrences do not build a report. static.cljs now passes the
cause so the exception page gets a full fingerprint.
Closes#11726
AI-assisted-by: deepseek-v4.1-flash
Shared-key callers (exporter, admin-console) arrive as keywords on
auth-key-id, so transit persisted them as ~:exporter while regular traffic
stored app. Coerce with d/name at the single origin so every audit and
telemetry copy carries a plain string. Adds regression tests for the origin
and the push-audit-events path, including caller spoofing precedence.
Closes#11628
AI-assisted-by: muse-spark-1.3-contributor
Migrate docker/images/Dockerfile.media-processor from ubuntu:26.04 to
dhi.io/node (Debian 13/trixie), which also drops the manual Node tarball
download since the base image ships it. The -dev tag stays as the final
image: fontforge, woff2 and the graphics libraries are needed at runtime.
scripts/build now assembles the release bundle under target/ (dist/ plus
the manifests and a generated setup script), the way the other modules do,
since esbuild leaves the runtime dependencies external. manage.sh gains
build-media-processor-bundle and build-media-processor-docker-image, both
wired into build-bundle and build-docker-images.
The CI workflows are intentionally left untouched: the module is still work
in progress and its images are not published yet, so this only enables
local builds."
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
sha is the full 40-char SHA in build-bundle.yml's output and
build-docker.yml's new checkout-pinning input, matching git and
GitHub's own convention. short_sha stays internal to each workflow
for the S3 keys and image tags that already used it — build-bundle.yml
no longer exposes it externally since nothing outside consumed it.
No S3 key, image tag, or marker value changes anywhere.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
sha is the full 40-char SHA in build-bundle.yml's output and
build-docker.yml's new checkout-pinning input, matching git and
GitHub's own convention. short_sha stays internal to each workflow
for the S3 keys and image tags that already used it — build-bundle.yml
no longer exposes it externally since nothing outside consumed it.
No S3 key, image tag, or marker value changes anywhere.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
Add a new memory file documenting the backend audit log system:
purpose, storage schema, RPC producers, frontend ingestion,
webhooks/error-reporter/telemetry consumers, and Nexus archival.
Also wire a reference to it from the backend core memory so it
is discoverable through the memory graph.
AI-assisted-by: longcat-2.0
* 🐛 Align WASM viewer layers during zoom
Keep the absolute WASM layer at the logical viewer size so its canvas CSS
box and hotspot SVG remain aligned as zoom changes.
Add a Playwright regression covering DOM bounds, drawing-buffer sizing, and
hotspot clicks at zoom 1 and below 1.
Closes#11689
AI-assisted-by: GPT-5
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
* 🐛 Align WASM zoom regression clicks
Use the rendered WASM DOM selector and design-space points mapped through
canvas bounds for visual clicks.
Wait for the canvas and SVG bounds before checking zoomed-out positions.
AI-assisted-by: GPT-5
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
* 🐛 Fix WASM zoom render wait
Use Screen2 coordinates for the visual interaction hotspot.
Wait for non-empty canvas pixels after each viewer render.
AI-assisted-by: GPT-5
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
* 🐛 Match WASM render marker in zoom spec
Require the expected frame screenshot marker before reading canvas pixels.
Keep resized buffers blocked until the new frame draws.
AI-assisted-by: GPT-5
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
* ✨ Run WASM zoom regression in DPR project
Move viewer zoom coverage under the render-wasm Playwright project.
Assert DPR-scaled buffers and use the stable Zoom out role.
AI-assisted-by: GPT-5
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
* 🐛 Simplify WASM render wait
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
* 🐛 Remove unreliable WASM viewer regression
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
---------
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
_DEVELOP and _STAGING chain build-bundle.yml -> build-docker.yml,
both invoked with gh_ref as a branch name. Each reusable workflow
re-resolves that branch with its own checkout, at its own time.
build-docker's checkout happens minutes after build-bundle's, after
new commits can have landed on the branch, so it can compute a
different sha than the one build-bundle actually bundled and
uploaded to S3.
The Prepare job then requests a key that was never uploaded:
fatal error: An error occurred (404) when calling the HeadObject
operation: Key "penpot-sha-<newer-sha>.zip" does not exist
Expose build-bundle.yml's resolved sha as a workflow_call output and
thread it through _DEVELOP, _STAGING and _ADHOC as build-docker.yml's
new optional sha input, which pins its checkout instead of
re-resolving gh_ref. gh_ref keeps naming the branch tag to move.
_TAG is unaffected: it builds from a pushed git tag, which cannot
move the way a branch can, so build-bundle and build-docker already
agree on one commit there.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
dhi.io/node:24.20.0-debian13-dev does not pre-create /usr/local/bin
the way a regular Debian image does, so tar -xzf ... -C /usr/local/bin
failed with "Cannot open: No such file or directory" right after the
checksum check passed. Introduced in #11790 when pnpm moved from
Corepack to a downloaded standalone binary.
Dockerfile.media-processor and docker/devenv/Dockerfile are unaffected:
both extract into /opt/node/bin, which already exists from the prior
Node.js install step.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
_DEVELOP and _STAGING chain build-bundle.yml -> build-docker.yml,
both invoked with gh_ref as a branch name. Each reusable workflow
re-resolves that branch with its own checkout, at its own time.
build-docker's checkout happens minutes after build-bundle's, after
new commits can have landed on the branch, so it can compute a
different sha than the one build-bundle actually bundled and
uploaded to S3.
The Prepare job then requests a key that was never uploaded:
fatal error: An error occurred (404) when calling the HeadObject
operation: Key "penpot-sha-<newer-sha>.zip" does not exist
Expose build-bundle.yml's resolved sha as a workflow_call output and
thread it through _DEVELOP, _STAGING and _ADHOC as build-docker.yml's
new optional sha input, which pins its checkout instead of
re-resolving gh_ref. gh_ref keeps naming the branch tag to move.
_TAG is unaffected: it builds from a pushed git tag, which cannot
move the way a branch can, so build-bundle and build-docker already
agree on one commit there.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
Mirrors the devenv change in 3385a65 (docker/devenv/docker-compose.infra.yml).
Mailpit keeps messages in memory only (no persistence), consistent with
mailcatcher's previous behavior; the volume and MP_DATABASE env var are
left commented for anyone who wants to opt in later. UI port changed from
1080 to Mailpit's native 8025.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
The seven creation commands no longer accept an optional client
id: create-file, create-project, create-team,
create-team-with-invitations, upload-file-media-object,
create-file-media-object-from-url and assemble-file-media-object.
The server always generates the identifier; a sent id is ignored.
Malli maps are open and the RPC layer never strips unknown params,
so the handlers that would still honor an id (create-file,
create-project) now drop it explicitly. Internal callers that pass
remapped ids (project duplicate, binfile import) keep working.
Closes#11783
AI-assisted-by: muse-spark-1.3-contributor
dhi.io/node:24.20.0-debian13-dev does not pre-create /usr/local/bin
the way a regular Debian image does, so tar -xzf ... -C /usr/local/bin
failed with "Cannot open: No such file or directory" right after the
checksum check passed. Introduced in #11790 when pnpm moved from
Corepack to a downloaded standalone binary.
Dockerfile.media-processor and docker/devenv/Dockerfile are unaffected:
both extract into /opt/node/bin, which already exists from the prior
Node.js install step.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
* 🐛 Bound decompressed size of JSON entries on binfile v3 import
Every JSON/text zip entry (manifest, files, pages, shapes, colors, components, typographies, tokens, plugin-data) was decompressed without any size limit, letting a small .penpot archive exhaust the backend heap (GHSA-qcw7-v626-g6cf). Only binary storage blobs were guarded. Reuse the existing size-limiting-stream guard on the text path: 20 MiB cap per entry, 200 MiB cumulative budget per import job, plus a cheap declared-size pre-check. Both limits are configurable and wired through the binfile, management and debug entry points. Adds zip-bomb regression tests for the file entry, the synchronous manifest read and the cumulative budget. Closes#11606
AI-assisted-by: muse-spark-1.3-contributor
* ♻️ Uniform binfile import limits behind init-limits
Move the binfile import limits to a single source of truth in app.binfile.common (default-* vars) and drop the duplicated entries from config/default; env overrides keep working through the schema. Resolve all limits once per job with init-limits (::max-size, ::total-max, ::current-size, ::max-object-size, ::max-zip-entries) instead of rebuilding the map per zip entry. Thread cfg as the first arg through the v3 readers, collapse read-plain-entry into read-entry, and give size-limiting-stream a single explicit-counter arity. v1 keeps using the compiled default (mechanical rename only). No behavior change. AI-assisted-by: muse-spark-1.3-contributor
* ♻️ Rename binfile limits to text-entry/binary-entry terms
Use text-entry/binary-entry vocabulary consistently across config keys, bfc input keys, default-* vars and the limits resolved by setup-limits (::max-text-entry-size, ::max-text-total-size, ::current-text-size, ::max-binary-entry-size). Rename init-limits to setup-limits. No behavior change. AI-assisted-by: muse-spark-1.3-contributor
* ♻️ Rename cumulative text counter and document binary limit
Rename ::current-text-size to ::accumulated-total-text-size for clarity and expand the default-max-binary-entry-size comment to match the other limit vars. No behavior change. AI-assisted-by: muse-spark-1.3-contributor
* ♻️ Harden binfile guards and prove budget accumulation
Add a regression test that only passes when text bytes accumulate across entries (budget between largest entry and summed total; verified red against a per-entry atom). Include the entry name in streaming-guard errors, count skipped bytes against the budget with a direct unit test, and forward all four limit keys in get-manifest. No behavior change. AI-assisted-by: muse-spark-1.3-contributor
* ♻️ Thread cfg through get-manifest
get-manifest now takes the caller cfg and resolves limits with setup-limits like the import job itself, instead of building a single-use mini-cfg from cf/get. No behavior change. AI-assisted-by: muse-spark-1.3-contributor
Lock in the fix from 31b73460c3 (#11803) with a regression
test for the exact reported scenario: loading the viewer
with a URL that already contains `zoom=fill`.
At 2.18.0-RC5 `update-zoom-querystring` navigated without
any comparison, so the load sequence bundle-fetched →
zoom-to-fill → update-zoom-querystring → nav → navigated
re-ran forever and crashed the page with React error #185
("maximum update depth exceeded"). The guard added in
31b73460c3 breaks the cycle; the new test asserts that a
bundle fetch against a `zoom=fill` route emits no
navigation events.
Also updates the dashboard/viewer frontend memory to
document the guard and the loop it prevents.
AI-assisted-by: glm-5.3-flash