Recent Activities
This page shows what are we working on.
-
head
-
Handle malformed URI sequences in tab anchor decoding 973dd6
-
Fix customer validation for guest users by handling missing group_id f06ace
-
Enhance Ajax tab filtering and validation, implement safe parameter handling in LoadOptions and NewConditionHtml, and ensure proper product checks in Index controller ead1aa
-
Fix missing aria-level on tab headings in expanded layout (#39)
* Fix missing aria-level on tab headings in expanded layout
* Fix missing aria-level attribute for expanded tabs on Breeze
---------
Co-authored-by: Vitaliy Dmytruk <vitaliy@swissuplabs.com> 3dab50 -
Fix missing aria-level attribute for expanded tabs on Breeze 05c0a6
-
Fix missing aria-level on tab headings in expanded layout 2523f0
-
-
1.13.6
-
Version 1.13.6 fe61d0
-
Fix product visibility bypass and GraphQL tab conditions bypass (#38)
* Fix product visibility bypass and GraphQL tab conditions bypass
- Helper\Product::canShow(): parent_id fallback now applies only to
enabled products and only when parent_id is their real parent
(catalog_product_relation). Previously any visible product id in
parent_id exposed disabled products via the easytabs AJAX endpoint.
- GraphQL getEasyTab now validates tab conditions (customer group, etc.)
using the customer group from GraphQL context, like the storefront does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Address review: stable parent check, duplicate aliases, real customer group
- Helper\Product: check parent's children via type instance
getChildrenIds() instead of Relation::getRelationsByChildren(),
whose return shape is not a stable API. Drops ObjectManager usage.
- GraphQL data provider: with duplicate aliases, return first tab that
passes conditions instead of failing on the highest-priority one.
- GraphQL resolver: load caller's customer group via customer repository
instead of defaulting to guest when context has no customer_group_id
(guest fallback exposed "not in group X" tabs on older Magento).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Pick same tab as storefront for duplicate aliases in GraphQL
Mirror Block\Tabs::_buildTabs(): among tabs passing conditions, prefer
the requested store view over default store view (0), and within a store
take the same tab the storefront does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> 90330e -
Pick same tab as storefront for duplicate aliases in GraphQL
Mirror Block\Tabs::_buildTabs(): among tabs passing conditions, prefer
the requested store view over default store view (0), and within a store
take the same tab the storefront does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> 06c0a8 -
Address review: stable parent check, duplicate aliases, real customer group
- Helper\Product: check parent's children via type instance
getChildrenIds() instead of Relation::getRelationsByChildren(),
whose return shape is not a stable API. Drops ObjectManager usage.
- GraphQL data provider: with duplicate aliases, return first tab that
passes conditions instead of failing on the highest-priority one.
- GraphQL resolver: load caller's customer group via customer repository
instead of defaulting to guest when context has no customer_group_id
(guest fallback exposed "not in group X" tabs on older Magento).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> 3871b9 -
Fix product visibility bypass and GraphQL tab conditions bypass
- Helper\Product::canShow(): parent_id fallback now applies only to
enabled products and only when parent_id is their real parent
(catalog_product_relation). Previously any visible product id in
parent_id exposed disabled products via the easytabs AJAX endpoint.
- GraphQL getEasyTab now validates tab conditions (customer group, etc.)
using the customer group from GraphQL context, like the storefront does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> 3a8cc0
-
-
head
-
fix(upload): upload one file at a time and stop once refused (#72)
Selecting thirty files is one click, and the widget fired one request per file
in parallel. With file_upload now metered, the first twenty are accepted and
every request after that counts as a rate-limit violation; ten violations block
the address for an hour, for ticket and message creation as well. A legitimate
bulk attachment locked the visitor out of the helpdesk entirely.
Chain the uploads and abandon the rest after the first server refusal, so a
bulk selection trips the limit once rather than a dozen times. A file rejected
by the client-side extension or size check still lets the others through.
The endpoint also reached Model\Upload with a null payload when a POST carried
a valid form key but no file, which is a TypeError and an HTML exception page
for an ajax call rather than the JSON refusal the uploader can show.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 09f477 -
fix(message): record backend authorship instead of inferring it (#71)
isTrustedAuthor() read user_id and an empty email_message_id off the stored
row. Neither says who wrote the text.
The Pick Up button on the ticket grid runs Cron\EmailStorage\PickUp inside the
administrator's own backend request, so ResourceModel\Ticket::_afterSave()
stamped that administrator's user_id onto every inbound message of the run.
_afterSave() never copies the inbound Message-Id, so email_message_id stayed
empty too: an e-mail body reached the CMS filter, raw HTML and {{...}}
directives included, which is the hole #71 is about. Adminhtml\TicketMessage\
AjaxSave gave the same pair to any existing message an administrator re-saved,
including a customer's.
In the other direction the notification observers write the outgoing mail's
Message-Id onto the message they report, so an administrator's own reply lost
its authorship the moment the customer notification went out, and the option
silently stopped working for the one case it exists for.
Record the fact instead. swissup_helpdesk_ticket_message.is_backend_authored is
set by the two paths that know the text came from a backend form -- the admin
ticket Save controller and AjaxSave creating a new message -- and read by the
policy. _afterSave() takes it from its caller rather than from the session.
Existing rows default to 0, so messages written before the upgrade render
escaped. No backfill: user_id plus an empty email_message_id is exactly the
inference this commit removes, and the option is off by default.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 284e11 -
fix(security): key rate limiting on the visitor, not the remote address (#74)
Every counter was keyed on RemoteAddress::getRemoteAddress(). Behind a proxy or
CDN — the default Magento setup, where remote_addr_headers is empty — that is one
address for the whole shop, so ten violations from a single abuser tripped
BLOCK_THRESHOLD and locked the helpdesk for every visitor for an hour. Configure
remote_addr_headers instead and the value comes from a client-supplied header,
which makes the limit bypassable by rotating it.
Counters are now keyed on the customer id, falling back to the session id, and
only to the remote address when there is no session at all. A shared address no
longer collects other people's violations, and a forwarded header no longer
resets anyone's bucket.
The trade-off: a guest who discards the session cookie between submissions starts
a fresh bucket, which an address-keyed counter would have caught on a shop with
direct connections. That case belongs to the CAPTCHA the module already wires
into both forms, not to a counter that can lock out real customers.
blockIp()/unblockIp()/isBlocked() keep their names — they are public API — but
now act on the resolved identifier, and the log context and the visitor-facing
message no longer say "IP address".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 541e3a -
fix(cron): keep e-mail pickup running when one message or mailbox fails (#74)
The try wrapped the whole loop over a mailbox and its catch did `return false`,
leaving execute() entirely. One message that threw — bad encoding, a missing
header, a failing save — therefore aborted the department it was in and every
department queued after it, on every cron run, until someone removed it by hand.
The catch is now per message, and fetching the message happens inside it, since
that is where a malformed message throws. Opening the mailbox is wrapped
separately so an unreachable IMAP host skips only its own department.
Messages are deleted in one sweep after the loop, so a department that failed on
any message keeps its whole mailbox and logs why. Catching and continuing without
that would have deleted the message that was never processed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 7aa1b7 -
fix(upload): log the attachments that receive no content inspection (#74)
validateFileContent() returns early for any extension outside KNOWN_MIME_TYPES,
so an extension the admin adds to the allow-list — xsl, csv, odt — reaches disk
with nothing but the extension whitelist behind it. The doc block already told
the reader a notice was logged in that case; nothing was.
The early return stays. Refusing unknown extensions outright would break every
installation that extended the list on purpose, and there is no execution path
to abuse today: attachments are written under var/, outside the document root,
and are served by Controller/TicketMessage/File with nosniff and a download
disposition. What was missing is the operator's ability to see it happening, so
the notice carries the file, the extension and the MIME that was sniffed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 813258 -
fix(admin): require the ticket ACL resource on the ticket grid controller (#74)
Grid declared neither ADMIN_RESOURCE nor _isAllowed(), so Magento fell back to
Magento_Backend::admin and any logged-in admin user could pull the ticket
listing, including the customer-filtered view, without the Swissup_Helpdesk::ticket
permission. Every other controller in Controller/Adminhtml declares one.
The unused Swissup\Askit import went with it; Askit is not a dependency of this
module and the symbol was never referenced.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 367d03 -
fix(file): sign attachment links with a real secret and stop previewing documents (#73)
The download link's hash was the sha256 of the message's created_at, with no
salt and no key. validateHash() takes the salt out of the hash it is handed, so
the timestamp was the whole secret — and it is printed next to every message as
"x minutes ago". Anyone who knew the message id, the stored path and roughly
when it was written could produce an accepted link, and the hash was the only
authorisation the action had.
Links are now signed with an HMAC over the message id and the file path, keyed
with the installation's crypt key. Every configured key is accepted on
validation and the newest one signs, so rotating the key does not break links
already sent out.
Inline preview no longer serves text/html or image/svg+xml. From the shop's own
origin those are documents that run script in the visitor's session, not
pictures; they are downloaded instead. What is still previewed is sent with
nosniff and a restrictive Content-Security-Policy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> ccccd9 -
fix(upload): require POST, a form key and the rate limit on the upload endpoint (#72)
The endpoint wrote a file to var/helpdesk with no checks at all: it ran on GET,
validated no form key, and never called the rate limiter, although a file_upload
bucket had been declared for it since the limiter was added. An anonymous client
could fill the volume in a loop, and the endpoint was reachable cross-site.
It now refuses anything but POST, validates the form key, applies the declared
limit, and — like the ticket and message controllers — only serves guests where
guests may open a ticket at all. Refusals come back as JSON with a 400 so the
uploader can show them, instead of an exception page for an ajax call.
The uploader's error branch was registered as `fail`, which jQuery.ajax never
calls, and used `this` where the widget was meant; it now reports the server's
message.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 5fd3ca -
fix(message): run only backend-authored text through the CMS filter (#71)
The CMS page filter renders raw HTML and executes {{...}} directives. Every
message went through it when the option was on, including text written by
customers, guests and inbound e-mail, which is a stored XSS path against the
customer and against any agent opening the ticket.
Authorship now decides, not the option alone: text an administrator wrote in
the backend is still rendered as rich content, everything else is escaped.
An administrator's address on an incoming e-mail does not count, because
From: is not authenticated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 11a308
-
-
1.4.10
-
Version 1.4.10 f95e5c
-
Merge pull request #76 from swissup/fix/inbound-email-sender-70
fix(cron): do not append inbound e-mail to a ticket the sender does not own (#70) 62b851 -
Merge pull request #75 from swissup/fix/ticket-reply-ownership-69
fix(message): reject replies to tickets the visitor does not own (#69) e35a26 -
Merge remote-tracking branch 'composer/master' into fix/ticket-reply-ownership-69
# Conflicts:
# Controller/TicketMessage/Save.php 5b8a62 -
Merge pull request #77 from swissup/chore/drop-non-english-comments
style: drop the Ukrainian and emoji comments from the security hardening d428a9 -
fix(cron): guard the remaining header accessors and the blacklist (#70)
Review of this branch found the "no longer throws on a missing From:" claim was
wrong, and that the blacklist had the same first-address weakness the ownership
check had just been fixed for.
`getFromAddresses()` was guarded, but `getFrom()` was not, and `PickUp` calls it
one line earlier — so a message with no From: still threw out of `execute()` and
still took pick-up for every remaining department with it (#74). `getSubject()`
had the same shape and is reached for the ticket number and the new ticket's
title, so a message with no Subject: did the same. Both return an empty string
now, and `getFromName()` goes through `getFrom()` rather than reading the header
itself.
The blacklist only ever saw the first address, so `From: clean@example.com,
blacklisted@evil.test` walked past it while the message was delivered from the
blacklisted mailbox. Every address in the header is checked now.
`getFromAddresses()` caches its result and `setMessage()` clears it, so the
header is parsed once per message instead of three times.
The test for a header-less message now covers every accessor the cron calls on
the parser, which is what would have caught `getFrom()`: it fails on the
previous commit with "Header with Name from or from not found" at Parser.php:243.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 175135 -
fix(i18n): add the ticket-not-available message to en_US.csv (#69)
The string this branch adds to `Controller/TicketMessage/Save` was the only
user-facing one in that controller missing from the translation file, so it
stayed English on every other locale.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 7cdd8c -
style: keep the single-line @var hints and the ResourceModel note
Review of this branch found the cleanup took three things it should not have.
The doc block tidier matched `/** ... */` with DOTALL, so a block that opens
and closes on one line looked like a block whose content had all been removed
and was dropped whole, whatever language it was in. That deleted all nine
`/** @var ... */` type hints in the controllers and the observer — the ones
PHPStan and the IDE need on `resultFactory->create()` and
`$transport->getMessage()`. The tidier now leaves single-line blocks alone.
`TicketDataFilter::ADMIN_ADDITIONAL_FIELDS` loses its Ukrainian notes but keeps
the fact they carried, in English: those four keys are consumed by
`ResourceModel\Ticket::_beforeSave()`/`_afterSave()`, and dropping them from the
whitelist silently discards the data before it reaches them. That is not
visible from this file.
`// varchar(128)` and `// primary key` are gone too. They had survived only
because they happened to be English while every neighbouring entry lost its
note, which read as arbitrary.
Still comment-only: with comments and whitespace removed, the token stream of
all 12 files is identical to master.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 0e443b -
style: drop the Ukrainian and emoji comments from the security hardening
The October hardening wave left its working notes in the code — Ukrainian
prose and ✅/⚠️ markers in `// ЗАХИСТ:` comments across the controllers, the
Service filters, the mail classes and the data patch. 120 comment lines are
removed and 61 more lose a Ukrainian trailing note or description.
Doc block tags keep their type and variable: a `@param array $fields` whose
only description was Ukrainian becomes `@param array $fields` rather than
disappearing, so no API documentation is lost. A block left with nothing but
tags keeps the leading `*` line this module already uses elsewhere.
Comment-only: with comments and whitespace removed, the token stream of all
12 files is byte-identical to master.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> fbc509 -
style(security): drop the explanatory comments from the access check (#69)
The reasoning stays in the commit messages and the pull request.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 9e649d -
style(security): drop the explanatory comments from the sender check (#70)
The reasoning stays in the commit messages and the pull request.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 94e19b -
fix(security): take the sender from the parsed From:, scope staff to the department (#70)
Review of this branch found the new check could be walked around, and that it
left the staff half of the finding open.
The sender was read with `preg_match('|<(.*?)>|')` over the raw From: header —
the first bracketed address wins. RFC 5322 allows more than one mailbox in
From:, and a display name may itself contain angle brackets, so either of
From: Owner <owner@example.com>, Attacker <attacker@evil.test>
From: "x <owner@example.com>" <attacker@evil.test>
made `getFromEmail()` return the victim's address while the message was
delivered from the attacker's, and the new ownership check then let it into the
victim's ticket. Verified against laminas-mail: both headers yield
`owner@example.com` from the old code.
`getFromAddresses()` now asks Laminas for the parsed address list and reports
every address it holds, so a header naming several mailboxes — or a header sent
twice — stays visible as more than one sender instead of collapsing to the
first. `hasSingleFromAddress()` is what the cron holds the message against: with
no single sender the message reaches no existing ticket, is never attributed to
staff, and opens a ticket of its own without being bound to a customer account.
`getFromEmail()` keeps its contract for the blacklist and the staff lookup, and
no longer throws when the header is absent, which used to abort pick-up for
every remaining department.
Staff status is no longer a blanket pass. It is decided against the users of the
department whose mailbox received the message, which says nothing about a ticket
belonging to another department, so `isAllowedOnTicket()` now also requires the
ticket to be that department's. Without this, any address matching any
department user reached every ticket in the shop and was stored as a staff
reply — finding 2 of the issue. The remaining exposure is an unauthenticated
From:, which needs SPF/DKIM at the MTA.
The new ticket opened for a rejected message now records in its notes which
ticket the message had referenced, so the two can be linked from the grid
rather than only from the log.
Parser tests skip where laminas/laminas-mail is not installed, as it is an
optional dependency.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 157509 -
fix(security): tighten the ticket access check after review (#69)
Three points from the review of this branch:
* The guest e-mail is now taken from the session only. The reply form never
posts an `email` field, so accepting one from the request added a way to
reply without ever passing through `Ticket/View`, where the address is
checked and remembered. Nothing legitimate used it.
* E-mail addresses are compared with `strcasecmp` instead of `===`. The
ticket keeps the address exactly as it was typed into the form, so an
owner coming back with a differently cased spelling was being refused.
This also makes the rule the same as the one `InboundMailSender` applies
to incoming mail.
* A non-string e-mail is treated as no e-mail rather than cast. `email[]=x`
arrives as an array, and `(string) $array` raises "Array to string
conversion", which Magento turns into an exception in developer mode — a
500 instead of a refusal.
The unit tests now set `customer_id` and `email` as data and let the real
Ticket accessors run, instead of mocking them. `getCustomerId()` casting to
int is what makes the strict comparison correct, and the previous mocks
would have stayed green without it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 3904ce -
fix(cron): do not append inbound e-mail to a ticket the sender does not own (#70)
Pick-up resolved the ticket from In-Reply-To or from the number in the
subject and appended the message without looking at the sender, so a
forged From: could post into another customer's ticket — as staff, if
the address matched a department user.
The sender now has to belong to the ticket. When it does not, the
reference is dropped and the message opens its own ticket, with a
warning in the log: nothing is lost and nothing lands in a conversation
it does not belong to.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 10123f -
feat(security): add sender check for inbound e-mail (#70)
Answers whether an unauthenticated From: may be appended to an existing
ticket: staff of the receiving department, the address the ticket was
opened with, or the account it belongs to. Covered by unit tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 22fee1 -
fix(message): reject replies to tickets the visitor does not own (#69)
The reply controller loaded the ticket by its public number and appended
the message without ever tying the ticket to the session, so any visitor
holding a valid form key could post into another customer's ticket.
The ticket now has to pass the same owner check the view controller
applies, and an unknown ticket and a foreign one give the same answer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 9428a3 -
refactor(ticket): take the view controller's owner check from the shared service (#69)
No behaviour change: the same two rules, now expressed once so the view
and the reply controller cannot drift apart.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 06c155 -
feat(security): add ticket access checker shared by view and reply (#69)
A single place that answers "may this visitor act on this ticket":
a customer ticket belongs to its customer, a guest ticket to whoever
presents the e-mail it was created with. Covered by unit tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 057646
-
-
head
-
Added missing `$.Event` be2d4d
-
Fixed missing child components at checkout page 37df49
-
Fixed checkout integration errors a45aac
-
uiComponents: Fixed empty provider when dataScope is not set a9dfcf
-
Prepare nested children as well. Fixes checkout flatrate validation 9905d7
-
Do not ignore rjsResolver anymore 7e40af
-
CheckoutLoader component 042f34
-
Align `$.Deferred` with jQuery API c9f87f
-
Align `$.request` with jQuery API 8aa15c
-
Improve `:visible` selector support 91a9ff
-
Fixed undefined child in elems when uiLayout component is not loaded yet 345194
-
Added `repeat` knockout binding (payment methods list) ba27e4
-
Wait for mixins before resolving a dependency e24fd4
-
Custom modal should not render lazily (checkout order summary) 3bfe11
-
Fixed infinite focus loop when validating single field (login popup) 59cd0c
-
Allow using `:visible` hack for multiple selectors 268c1a
-
Fixed mixins not applied to module dependencies when loaded asynchronously 5d91b8
-
Basic implmentation of `ko.datepicker` 05df54
-
Align date with moment.js 8ce8c3
-
Align calendar with jQueryUI f1cd4a
-
Align ui cmp initialization with Magento. Do not pass element.
This fixes `google-pay-cart` logic. d292d3 -
Implement `ko.fadeVisible` 4c14f6
-
Implement simplest version of `rjsResolver` 3440bb
-
Fix possible js error: read source synchronously first a77cb4
-
Align js cmp with Luma: Don't overwrite existing fn with `undefined` baea61
-
Added `validation.check` method 531321
-
`$.grep` implementation c5ec90
-
Fixed possible js error `Cannot read properties of undefined` fb7bef
-
Do not load js component if it uses `componentDisabled: true` cd0baf
-
-
2.32.1
-
Version 2.32.1 c5afe6
-
Prevent infinite loop when head contains invalid stylesheet link 582a77
-
Fixed incorrect magnifier image when using expanded gallery fc2f6b
-
Prevent auth popup form submit when recaptcha is not ready yet
Closes #101 740ada -
Fixed incorrectly marked lazyload when main image is not the first one d5cd6b
-
Do not add dom duplicates to $.registry 84beb6
-
-
1.2.0
-
Added Apple login, see #7 3fedc9
-
-
1.1.13
-
1.1.0
-
Added Apple login, see swissup/module-social-login#7 63067b
-
-
head
-
Improve isApiRequest logic so it don't return true on direct link 087787
-
-
1.2.4
-
Version 1.2.4 a81b40
-
Merge pull request #5 from swissup/fix/password-injection
Fix password injection: don't read registration password from session in repository plugin bef991 -
Password injection vulnerability fix: don't read password from session
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> fab4c1
-
-
head
-
Do not change header cart styles on the checkout page 7d5dec
-
-
1.36.1
-
head
-
1.0.10
-
Version 1.0.10 b8b88d
-
Fix child labels ignoring schedule, store and product status
- Skip expired labels and pass active period to frontend
- Filter index by store view, skip disabled and out-of-website children
- Validate $mode before using it in SQL column names
- Require swissup/module-pro-labels ^1.10.0 (active period columns) 3074b5
-
-
head
-
1.10.3
-
Version 1.10.3 fee5d6
-
Fix stored XSS in category labels data-mage-init JSON (#50)
Labels JSON is output in a single-quoted data-mage-init attribute. The
browser decodes HTML entities in attribute values before JS reads them,
so escaped variable values (<img onerror=..>) and entity-encoded
label text turned into live HTML and were inserted via insertAdjacentHTML.
" could also break out of the JSON and inject other mage-init
components.
Encode with JSON_HEX_AMP | JSON_HEX_TAG so no "&", "<", ">" reach HTML.
Values decode identically in JSON.parse; script-context consumers
(x-magento-init) are unaffected.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> bc43fd -
Fix stored XSS in category labels data-mage-init JSON
Labels JSON is output in a single-quoted data-mage-init attribute. The
browser decodes HTML entities in attribute values before JS reads them,
so escaped variable values (<img onerror=..>) and entity-encoded
label text turned into live HTML and were inserted via insertAdjacentHTML.
" could also break out of the JSON and inject other mage-init
components.
Encode with JSON_HEX_AMP | JSON_HEX_TAG so no "&", "<", ">" reach HTML.
Values decode identically in JSON.parse; script-context consumers
(x-magento-init) are unaffected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> a1aeee -
Force string type for typed functions.
https://app.basecamp.com/6059321/buckets/44137853/todos/10378503157#__recording_10383529593 d5c6c0 -
Implement label content validation and enhance security for label rendering b73f5a
-
Merge tag '1.10.2'
1.10.2 d539b4
-