BadResponse and its subclasses (ServerError, BadGateway, BadRequest, Conflict, Forbidden, NotFound, NotImplemented, Unauthorized, exported from @arkyn/server) no longer include a cause field in the JSON response body when NODE_ENV === "production". Previously cause was always included, in every environment, which could expose stack traces, SQL fragments, or other internal error detail to clients in production. UnprocessableEntity is the one exception, it still includes cause in production, its cause is now redacted, structured field/validation data intended for the client (see the v3.x.x changelog), not internal diagnostic detail.error.cause from a production error response body (any status other than 422), it will now be undefined there. It's still present in development, and in any NODE_ENV value other than "production". message and name are unaffected in every environment.ts
// In production, this response body no longer has a "cause" key// (except for UnprocessableEntity's 422 responses)throw new ServerError("Database operation failed", { table: "users" });
ts
// If you depend on cause reaching the client, either move that data// into "message", or keep it in a 422 via UnprocessableEntity's// redacted "data"/"fields", which are still sent in production.throw new UnprocessableEntity({message: "Database operation failed",data: { table: "users" },});
cause was intended for local debugging, not client-facing logic. Anyone parsing cause specifically in production error-handling code needs to stop, or move that dependency to a 422 (UnprocessableEntity) response, where it's preserved (redacted).UnprocessableEntity's own cause (fields/data) is now redacted with parseSensitiveData regardless of environment, independently of this production-hiding change.LogService.getConfig() (exported from @arkyn/server) now types apiUrl as string | null instead of string. This is a consequence of removing a hardcoded, plain-http:// fallback endpoint: when no valid logBaseApiUrl (an https:// URL, or http://localhost/http://127.0.0.1 for local dev) has been configured, apiUrl is null and no log requests are sent, rather than silently pointing at Arkyn's old default log server.LogService.getConfig()?.apiUrl and assumed it was always a non-null string, add a null check:ts
const config = LogService.getConfig();console.log(config.apiUrl.toUpperCase());
ts
const config = LogService.getConfig();if (config?.apiUrl) {console.log(config.apiUrl.toUpperCase());}
LogService.setConfig({ logBaseApiUrl: ... }), configure an explicit https:// endpoint, the old hardcoded fallback no longer exists:ts
LogService.setConfig({trafficSourceId: "your-traffic-source-id",userToken: "your-user-token",logBaseApiUrl: "https://your-log-server.example.com",});
LogService.setConfig() and never read getConfig().apiUrl directly. TypeScript will flag any direct property access on apiUrl that isn't null-checked. Anyone who never configured logBaseApiUrl and depended (knowingly or not) on logs still reaching Arkyn's old default server needs to set an explicit, secure endpoint now, nothing is sent otherwise.ApiService's debug output, are now redacted with parseSensitiveData before being sent or logged. See the v3.x.x changelog for the full v3.0.9 release notes, including 17 other non-breaking security, bug, and accessibility fixes in the same release.RichText's video insertion modal now requires the URL to use https://, in addition to the ?v= parameter check it already performed. A YouTube URL passed as http://... is no longer accepted.http://..., use the https:// form instead, YouTube itself serves both, so this is typically just a matter of the URL string used.RichText's video insertion modal, and only when the pasted URL explicitly uses the http:// scheme. Video embeds already saved from previous versions are unaffected, this only gates new insertions made through the modal.https:// check now also gates the new link insertion feature introduced in v3.0.5. See the v3.x.x changelog for the full v3.0.5 release notes.PhoneInput's controlled-value masking (introduced in v3.0.3) no longer misplaces the country dial-code digits when a value string is in international format (starting with +), for example when onChange's value is round-tripped straight back as value, a common controlled-input pattern in React. The dial code is now stripped before masking; values without a leading + are unaffected.tsx
const [value, setValue] = useState("");<PhoneInput name="phone" value={value} onChange={setValue} />
"34999998888" fed onChange's "5534999998888" back into value, and the dial-code digits were read as part of the local number, shifting the display. After (v3.0.4), the same code shows "(34) 99999-8888" correctly.PhoneInput usage where value is a string starting with + (typically the onChange round-trip pattern). Consumers passing local digits without a leading +, the pattern already documented as of v3.0.3, see no change. This is best understood as a bug fix for a defect introduced by v3.0.3's masking change, not a deliberate behavior change.PhoneInput's controlled value is now formatted with the selected country's phone mask before being displayed. Previously, a controlled value rendered exactly as passed, while uncontrolled typing already applied the mask, so the two modes were inconsistent.PhoneInput value elsewhere and expected it unmasked.tsx
<PhoneInput name="phone" value="34999998888" />// Rendered: "34999998888"
tsx
<PhoneInput name="phone" value="34999998888" />// Renders: "(34) 99999-8888"
value to compensate for the old behavior, it's now redundant, non-digits are stripped before re-masking, so an already-formatted string still works the same way, and can be removed.PhoneInput in controlled mode (value prop). It doesn't break compilation or throw at runtime, it's a rendered-output change only, but it can break snapshot tests or surprise consumers relying on the old, unmasked display. There's no prop to opt back into the previous, unmasked behavior.schema prop, used by every Alert component (AlertContainer, AlertContent, AlertDescription, AlertIcon, and AlertTitle), was renamed to scheme. This change unifies the naming already adopted by Button, Badge, and IconButton across the library.schema to scheme in every use of Alert.tsx
<AlertContainer schema="danger">...</AlertContainer>
tsx
<AlertContainer scheme="danger">...</AlertContainer>
schema prop. The old name is no longer recognized and is silently ignored, Alert still renders, but without applying the expected color scheme.scheme pattern was already used by the library's other components; this change simply aligns Alert with the existing convention.@arkyn/components, framer-motion, mapbox-gl, slate, slate-history, slate-react, @react-google-maps/api, react-hot-toast, react-scroll, html-react-parser, and @react-input/mask, are no longer installed automatically and are now peerDependencies. In addition, the minimum required version of react and react-dom was corrected.bash
bun add framer-motion mapbox-gl slate slate-history slate-react \@react-google-maps/api react-hot-toast react-scroll html-react-parser \@react-input/mask
framer-motion for Drawer/Modal, mapbox-gl for MapView, slate* for RichText).react/react-dom requirement was corrected from >=19.2.6 to >=18.0.0, restoring compatibility with React 18 projects that had been incorrectly blocked.defaultValue prop on FullCalendar, used to set the initially focused date, was renamed to defaultViewValue.tsx
<FullCalendar defaultValue={new Date(2026, 5, 1)} />
tsx
<FullCalendar defaultViewValue={new Date(2026, 5, 1)} />
FullCalendar with the defaultValue prop to control the initially displayed date. The old prop is silently ignored in this version, without the rename, the calendar always opens on the current date.FullCalendar and Calendar: value controls the selection, viewValue controls the displayed period, and defaultViewValue sets the initial period.date property on event objects (FullCalendarEvent) was renamed to initialDate.defaultView prop, which set the initial view ("day" | "week" | "month"), was removed, the component now always initializes in month view.onChange prop was removed.date to initialDate on every event:tsx
events={[{ title: "Meeting", date: new Date(2026, 5, 23, 10, 0) }]}
tsx
events={[{ title: "Meeting", initialDate: new Date(2026, 5, 23, 10, 0) }]}
defaultView="day" or defaultView="week", remove the prop, there is no direct replacement; the calendar always opens in month view.onChange to react to date changes, replace it with onClickDate (user click on a cell) or onChangeView (navigation between periods), depending on the intended behavior:tsx
<FullCalendar onChange={(date) => setSelectedDate(date)} />
tsx
<FullCalendar onClickDate={(date) => setSelectedDate(date)} />
FullCalendar since its introduction in the previous version (v3.0.1-beta.146). Anyone who hadn't yet used the component is not impacted.blockedTimestamps and onClickDate, which expand control over calendar interactions. The same version also made "vertical" the default orientation for ImageUpload, MultiSelect, RadioGroup, RichText, and Select, that part does not require a code change and is therefore not listed as a breaking change here; see changelog for details.