Thumbnail :uri values arrive from the server over transit, so
media-ids decode to UUID objects instead of strings. The
resolved-uri? helper added in #11563 called
clojure.string/starts-with? on them unconditionally, raising
TypeError: str.lastIndexOf is not a function and crashing the
workspace on frame render. Guard with string? so non-string
URIs fall through to resolve-media, which stringifies them.
AI-assisted-by: muse-spark-1.3-contributor
The management API shared-key-auth middleware was using the standard = operator for key comparison, which is vulnerable to timing attacks. The RPC middleware already uses constant-time comparison via MessageDigest/isEqual.
This change:
- Makes constant-time-eq? public in app.http.middleware
- Updates app.http.management/shared-key-auth to use mw/constant-time-eq?
- Fixes an inconsistency where the nil-key branch returned a 2-arg function
- Adds comprehensive tests for the management shared-key-auth middleware
Closes#11426
AI-assisted-by: qwen3.7-plus
The workspace-thumbnail-by-id ref unconditionally called resolve-media
on thumbnail URIs, which caused a stack overflow when the URI was a
data URI (which can be megabytes long for large images).
Data URIs contain thousands of '/' characters (base64 uses '/' as one
of its 64 characters), causing lambdaisland.uri/join to iterate
thousands of times in remove-dot-segments and overflow the JavaScript
call stack.
Add a resolved-uri? helper that checks if the URI already starts with
'blob:' or 'data:', and skip resolve-media for those cases. Only call
resolve-media when the URI is a plain UUID (media-id from the server).
Closes#11562
AI-assisted-by: qwen3.7-plus
* ✨ Add expires-in option to create-demo-profile
Allow passing an optional expires-in duration when creating a demo profile so its purge is scheduled sooner than the global deletion delay. Values below 5 minutes or above the global delay are rejected with an invalid-expires-in validation error, resolved before any profile is created.
Closes#11573
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Make duration schema decoding total instead of throwing
parse-duration returned by the duration schema decoder threw DateTimeParseException on invalid strings, escaping params validation as a raw error. It now returns the input unchanged so invalid values fail the duration predicate with a clean params-validation error. Closes#11573 AI-assisted-by: muse-spark-1.3-contributor
* 📎 Fix doc version for expires-in change entry
The expires-in change entry was documented under 2.20 but the current version is 2.18.
AI-assisted-by: muse-spark-1.3-contributor
* 🔥 Remove unused comment profiles fan-out in dashboard
The dashboard event retrieve-unread-comment-threads issued one
get-profiles-for-file-comments call per distinct file id and stored
the result in :current-team-comments-users, a state key no one
reads. The dashboard renders each thread author from the thread
payload via get-owner, so the fan-out was N+1 dead work.
Drop the per-file branch and the state write; the event now issues
a single get-unread-comment-threads call.
AI-assisted-by: deepseek-flash
* 🔥 Remove unused file comments users event and state
fetch-file-comments-users had no callers and passed a :team-id
where get-profiles-for-file-comments requires :file-id. Its only
effect was writing :file-comments-users, a viewer state key no
one reads.
Remove the event and the unused state key. The viewer still
loads comment profiles through fetch-profiles.
AI-assisted-by: deepseek-flash
* 🐛 Fix email already used showing a toast instead of an input error
Registering with an email that already exists reported the problem with a
toast while the email input stayed in its valid state, which reads as if the
form was accepted. The message now lands on the email input itself, the same
way the recovery and change password forms report server side errors.
The error is written to :extra-errors because the form mutator recomputes
:errors from the schema on every change, so a value written there is dropped
on the next render.
Fixes#10890
* 📎 Update CHANGES.md to remove email registration fix
Removed a note about a fix for email registration error display.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
---------
Co-authored-by: Shreyash Agare <agareshreyash26@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
Skip the limited newsletter report when the public-uri host
belongs to penpot.dev or penpot.app, so the SaaS never sends
subscriber emails to its own telemetry endpoint.
Defer the subscriptions query with delay so it only runs when
a report is actually going to be sent.
AI-assisted-by: muse-spark-1.3-contributor
Delegating plan and review to a subagent (engineer-* or the
builtin general) starts a clean context instead of growing the
main session. Delegating to general keeps the same model.
AI-assisted-by: muse-spark-1.3-contributor
Turn the skills-only file into a full intro to opencode inside
plain devenv: setup, providers, models, opencode.json example,
gh auth, flows, and a skills summary at the end.
Provider, model, and flow sections follow Andrey's own setup
notes; the FAQ stays as a stub for later.
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Fix nested board drop target ignoring ancestor clip bounds
Frame hit-testing (get-frame-by-position, get-frames-by-position and
top-nested-frame) only checked a candidate board's own rectangle,
without accounting for an ancestor board with clip content enabled.
A nested board wider/taller than its clipping ancestor could still be
picked as the drop target in its invisible, clipped-away area, so a
dragged shape would get reparented there and disappear from view.
Add clipped-by-ancestor? to reject a point when it falls outside the
bounds of any ancestor board that has clip content enabled, so the
lookup now stops at the correct visible ancestor instead of
descending into the hidden region.
* 🐛 Fix Ctrl+click deep-select reaching into clipped board area
The clip-aware quadtree query (query-index) filters candidate shapes
by whether they overlap every clip-parent ancestor, but the whole
filter was skipped whenever clip-children? was false. That flag is
turned off while a modifier key (Ctrl/Cmd) is held for deep/penetrate
selection, which was meant to let it reach past boolean/mask clip
boundaries, but it also disabled enforcement for board "Clip content"
ancestors, letting a modifier-held click select a shape sitting in a
board's invisible, clipped-away region.
overlaps-parent? now only relaxes the check for non-frame clip-parents
(bool shapes / mask children) when clip-children? is false; board clip
ancestors are always enforced regardless of the modifier key.
The button let users convert a text shape's inline styles into a
typography asset even when the shape's font-id couldn't be resolved
(e.g. a custom/team font that was removed or isn't loaded), silently
baking a missing font into the new typography asset.
Guard the button on the font actually resolving via
app.main.fonts/fontsdb, in addition to the existing checks (no
typography or token already applied, single selection).
Added e2e coverage for all four conditions that must independently
hide the button: missing font, applied typography asset, multiple
selection with differing values, and applied typography token.
AI-assisted-by: claude-sonnet-5
The project title's max-width was capped via an inline style computed
from the number of thumbnail columns fitting in the grid below it (an
unrelated value, reused only because it happened to be in scope). This
produced an oversized gap between a short/medium title and the file
count, timestamp, and action buttons, and gave long titles an
arbitrary, columns-based truncation point unrelated to the row's
actual available width.
Replace it with a standard flexbox truncate-to-fit: the title sizes to
its own content and sits right next to the info/actions, only
shrinking (and ellipsizing) once the row runs out of room, while the
info/actions never shrink.
* 🐛 Reject duplicate chunk index in chunked uploads
Repeat uploads of the same chunk index each stored a new
object because upload-chunk only checked index bounds. Run the
handler in a transaction, lock the session row and reject an
already-stored index with :duplicate-chunk-index.
Also harden assemble-chunks to require exactly indices 0..n-1
so gaps or duplicates fail instead of assembling a corrupt
file. Covers media, fonts and binfile through the shared
helper.
Closes#11634
AI-assisted-by: muse-spark-1.3-contributor
* ✨ Cap upload chunk size at 30 MiB by default
Chunks were only bounded by the 350 MiB HTTP body limit while the
30 MiB caps applied to the assembled file. Add :upload-max-chunk-size
(default 30 MiB, tunable via env) and reject oversize chunks in
upload-chunk with :validation/:chunk-too-large before anything is
stored. App clients slice at 25/10 MiB, so no frontend change needed.
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Fix tx-run! call and storage resolve in upload-chunk
Pass cfg as first arg to db/tx-run!, which expects [system f & params; without it every chunk upload raised invalid system/cfg provided and no chunk was stored, breaking assemble with missing-chunks. Also resolve storage without reuse-conn: put-object! writes to the backend outside any transaction, so reusing the tx connection gives no atomicity. Media, font and storage suites green, lint and format clean. AI-assisted-by: muse-spark-1.3-contributor
Add a body line-length validator to scripts/check-commit. It
fails when a body line exceeds 76 characters, exempting
trailers, URLs, and unbreakable tokens. The 76 limit leaves
room for git log's four-space indent in an 80-column
terminal.
Align the subject limit with the documented 70 characters;
the checker allowed 90 before.
Document the rule as a hard, verifiable requirement in
AGENTS.md, CONTRIBUTING.md, the create-commit skill, and
the workflow memory, and point at scripts/check-commit.
Add tests for the validator and the subject length rule.
AI-assisted-by: deepseek-flash
AA clip on the Current→atlas blit softens shared tile edges so
the canvas background shows through as 1px lines at the 512px
grid when tiles are composed with SrcOver.
Closes#11638
Move build-docker and build-docker-devenv jobs from penpot-extended-runner
to penpot-standar-runner, point tests-exporter at the canonical
penpot-extended-runner label instead of the stale penpot-runner-02 alias,
and switch build-tag/release notify jobs from ubuntu-24.04 to ubuntu-latest.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
Move build-docker and build-docker-devenv jobs from penpot-extended-runner
to penpot-standar-runner, point tests-exporter at the canonical
penpot-extended-runner label instead of the stale penpot-runner-02 alias,
and switch build-tag/release notify jobs from ubuntu-24.04 to ubuntu-latest.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
Move build-docker and build-docker-devenv jobs from penpot-extended-runner
to penpot-standar-runner, point tests-exporter at the canonical
penpot-extended-runner label instead of the stale penpot-runner-02 alias,
and switch build-tag/release notify jobs from ubuntu-24.04 to ubuntu-latest.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
Cached Skia paragraphs bake absolute image/gradient shaders at layout
time. On move, clones reuse those paragraphs while painting at the new
selrect, so glyphs move and the fill stays put. Record the paint origin
when layout is built and translate the canvas when painting from cache
so shaders track the text. Also sync bounds before update_layout so
fills bake against the current container.
Rewrite sql:file-comment-users to join comment with
comment_thread and union the requesting profile id, then
join the resulting small id set against profile.
The previous "id IN (subquery) OR id = ?" forced a
sequential scan over the whole profile table with a hashed
subplan filter, taking ~1.9s on large instances. The
semi-join lets the planner use profile_pkey, dropping the
query to sub-millisecond time. UNION (not UNION ALL) keeps
the previous dedup semantics when the requesting profile is
also a commenter.
AI-assisted-by: deepseek-flash
* ♻️ Consolidate HIGHLIGHTS.md into CHANGES.md 🚀 section
Eliminate the redundant HIGHLIGHTS.md file and make CHANGES.md
the single source of truth for version highlights.
- Add 🚀 section for 2.15.0 (MCP server integration)
- Add 4 missing highlight entries to 2.17.0 🚀 section
- Rewrite frontend parser to extract from CHANGES.md 🚀
subsections instead of flat HIGHLIGHTS.md format
- Decouple parse-latest-released-version from highlights
extraction so it works independently of 🚀 content
- Conditionally render highlights section in modal when non-empty
- Rewrite tests for new parser behavior (11 tests, 21 assertions)
- Delete HIGHLIGHTS.md and remove .gitignore exception
- Add step 8b to update-changelog skill for proactively
proposing highlights during release workflows
- Add missing-highlights and missing-highlight-reference
anomaly types to the changelog anomaly report script
Closes#11530
AI-assisted-by: qwen3.7-plus
* ♻️ Use consistent string library and add multi-version test
Address code review findings:
- Use str/split (cuerdas) consistently in extract-rocket-items
instead of mixing cstr/split (clojure.string)
- Add parse-highlights-extracts-multiple-versions test to verify
the parser correctly extracts 🚀 items from multiple
versions in a single CHANGES.md body
AI-assisted-by: qwen3.7-plus
* ♻️ Scope 🚀 checks to X.Y.0 and split gaps from anomalies
Type C now only checks released X.Y.0 versions, since patches never carry 🚀 subsections by design. Type D requires both issue AND PR references with exact format, accepting multi-PR entries. C/D are reported as highlight gaps in their own section and no longer count toward the anomaly total. Key Principles and anomaly definitions updated to match. Addresses review comments on PR #11531.
AI-assisted-by: muse-spark-1.3-contributor
* ✨ Render markdown links and bold in check-updates highlights
The highlights modal showed raw markdown from CHANGES.md 🚀 lines (brackets and URLs). Add a pure parse-highlight-item parser for inline links and bold, render fragments with literal hiccup in the modal (links open in a new tab), and style links and strong elements. Non-http URLs and malformed markup degrade to plain text. Adds 12 unit tests.
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Point full changelog link to main instead of staging
The view-changelog button in the check-updates modal linked to the staging branch. Point it to main, which holds the published changelog. Version detection still fetches from staging.
AI-assisted-by: muse-spark-1.3-contributor
Fix LDAP injection vulnerability (T5-N1-03) where the client-supplied email was used directly in the LDAP search filter without escaping RFC 4515 special characters (*, (, ), \, NUL), and the profile email was taken from client input instead of the LDAP directory attribute.
Changes:
- Add escape-ldap-filter-value per RFC 4515 section 3
- Apply escaping in search-user before building LDAP filter
- Add get-attr helper for multi-valued LDAP attributes
- Fix retrieve-user to use directory email (attrs-email) instead of client email
- Use cuerdas.core instead of clojure.string
Closes#11084
AI-assisted-by: mimo-v2.5-pro
SkSVGDevice drops paint image-filters, so re-emit visible layer blur as a
native feGaussianBlur filter on the composite <g>. Match canvas sigma via
radius_to_sigma(value * scale), and skip Skia blur filters on the SVG
VectorRenderer path so shapes do not vanish.
Closes#11380
* 🐛 Fix fontFamilies token property mapping in Plugin API
The Plugin API exposes the font-family token property as `fontFamilies`,
while Penpot stores the canonical applied-token attribute as
`:font-family`. The bidirectional plugin/internal attribute map did not
contain that alias, so explicit `applyToken(..., ["fontFamilies"])`
validation rejected the property and applied-token readback exposed the
undocumented singular `fontFamily`.
Add `:font-family -> :font-families` to the existing canonical alias
map. The reverse mapping is derived automatically, keeping application
and readback symmetric without introducing a font-specific code path.
Closes#11405
AI-assisted-by: Omen Alpha
Signed-off-by: 최준수 <junsoo1172@gmail.com>
* 🐛 Fix fontFamilies e2e test to target a text shape
The fontFamilies end-to-end regression created a flex layout frame,
whose attribute set (frame-with-layout-attributes) excludes
:font-family. The workspace token application filters such shapes,
so the internal binding and readback assertions would pass vacuously
without exercising the alias.
Target an actual `:text` shape (ctho/add-text) instead, so the test
verifies the full JS "fontFamilies" -> schema -> alias -> canonical
:font-family -> camelCase readback path.
AI-assisted-by: Omen Alpha
Signed-off-by: 최준수 <junsoo1172@gmail.com>
* 🐛 Fix fontFamilies test WASM error and add changelog entry
The text-shape fontFamilies e2e applies a layout-affecting token via
wasm renderer path, hitting missing WASM exports under Node. Merge
thw/setup-wasm-mocks! into the :each fixture and add plugins
CHANGELOG entry for the fontFamilies alias fix.
AI-assisted-by: muse-spark-1.3-contributor
Related to #11566
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
---------
Signed-off-by: 최준수 <junsoo1172@gmail.com>
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
Closes#11384
Skia's SVG backend drops save_layer+SrcIn, so image strokes are re-emitted
as a linked <image> clipped to an opaque stroke silhouette (filled outline,
clip-rule evenodd). Open-path caps join the silhouette and grow the image
dest by cap_bounds_margin so markers stay textured.
Added a new section for Penpot Enterprise detailing its features and benefits for organizations.
Signed-off-by: Elenzakaleidos <elena.scilinguo@kaleidos.net>
* ✨ Add new slides content for 2.18 release
* ♻️ Use buttons from DS
* ♻️ Use new SCSS guidelines
* ♻️ Use a base stylesheet for all version files
* ♻️ Use new SCSS guidelines
* ✨ Add new images and wording
---------
Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>
Store the hide-resolved filter in user storage and restore it when
entering the workspace or viewer, consistent with canvas comment
visibility from #10239. Match the comments filter separator styling to
the main menu and add the missing mentions option in the viewer
dropdown.
Closes#10686
Signed-off-by: Andres Gonzalez <andres.gonzalez79@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>