Commit Graph

2389 Commits

Author SHA1 Message Date
lebaudantoine bd7c76302d wip not to be merged 2026-08-26 13:51:16 +02:00
lebaudantoine 325273e3c0 fixup! (backend) introduce a token exchange endpoint for iframe embeds 2026-08-26 13:51:16 +02:00
lebaudantoine 1378e59f75 🔒️(frontend) restrict transit_code exchange to embedded context
Only run the transit_code exchange flow when the app is loaded in an
embedded context (i.e. inside an iframe).

Combined with the CSP rules that will restrict which origins are
allowed to embed the app, this gives us a client-side lever to
control which integrations can actually use this authentication
path.
2026-08-26 13:51:16 +02:00
lebaudantoine d55d131dc6 🔒️(backend) bind accepted lobby entries to the current username
Bind an accepted lobby entry to the username the participant had at
the moment of acceptance.

This prevents a participant, once accepted, from changing their
display name and reusing the same lobby grant to enter the room
under a different identity.
2026-08-26 13:51:16 +02:00
lebaudantoine c7aa3c45cf 🔥(frontend) remove forgotten console.log calls
Drop leftover `console.log` calls that were accidentally left in the
frontend code and add noise to the browser console.
2026-08-26 13:51:15 +02:00
lebaudantoine 4dee35a0bb ♻️(all) stop relying on cookies for the lobby flow
The lobby system relied on cookies to identify the participant
across the wait/enter cycle, which does not work in an iframe
context where our cookies are dropped.

Simplify the lobby behavior:

* The POST request that enters the lobby now returns the
  participant id in the response.
* The frontend passes that id back on subsequent requests to keep a
  sticky session while trying to enter the room.

This moves a bit more logic to the frontend but should be a
transparent refactoring, without decreasing the security of the
lobby flow.
2026-08-26 13:51:13 +02:00
lebaudantoine 4c3ba6c0c3 🩹(frontend) unblock virtual background loading under bearer auth
Moving off cookie-based authentication surfaced several hard
issues, especially around loading virtual backgrounds: requests
used to be sent with cookies automatically, which trivially
authenticated those loads. With bearer tokens, those requests need
to be authenticated explicitly.

The situation is made harder by the fact that, when the custom
virtual background was introduced, some of the loading was done as
module-level, blocking imports that are not handled by React and
therefore live outside the normal auth flow.

Ship a functional patch to unblock third parties currently waiting
on this integration. The virtual background loading path should
definitely be refactored and simplified in a follow-up.
2026-08-26 12:17:35 +02:00
lebaudantoine 6df7752c06 (frontend) support alternative auth via URL fragment
Wire the frontend to the backend's token exchange flow: when the
expected URL fragment is present, gate the app loading on exchanging
that fragment for a proper access token, which is then used to
interact with the API.

When no such fragment is present, the code path is a no-op and
should have minimal impact on load performance.
2026-08-26 12:17:35 +02:00
lebaudantoine 3ad34f176d ♻️(backend) use a dedicated auth scheme for LiveKit token auth
We now use `Authorization: Bearer <token>` to authenticate users
from the token exchange flow (used for iframe embeds).

Until now, the `Bearer` scheme was also reused for the alternative
LiveKit authentication, where a client presents its LiveKit token
issued by the backend to prove room membership on actions open to
any room participant. Sharing the scheme between the two flows is
not viable anymore.

Switch the LiveKit token authentication to a dedicated
`Authorization` scheme, so `Bearer` stays reserved for the iframe /
token-exchange flow.

Follow-up: a broader effort should look into harmonizing and
hardening the backend authentication stack of the app.
2026-08-26 12:17:35 +02:00
lebaudantoine fcb7087f4d (backend) introduce a token exchange endpoint for iframe embeds
Some integrators render our videoconference inside an iframe, where
our cookie-based authentication does not work: our cookies are
SameSite=Lax/Strict, so the iframe drops them.

We looked at what Jitsi offers: a shared secret used to sign JWTs
that authenticate users coming from external services. Since we
already expose an external API where third parties authenticate as
a given user, it was simpler for us to add an exchange mechanism on
top of that.

Flow:

* Through the external API, mint a short-lived, single-use exchange
  code for a user.
* The third party hands that code to the frontend as a URL fragment.
* The frontend exchanges the code for a longer-lived JWT that can be
  used to query the regular API viewsets.

Known limitations and follow-ups:

* At some point it would be nice to shorten the JWT lifetime and
  add a refresh mechanism. This will be handled in a follow-up PR
  when actually needed.
* CSP rules to control which origins are allowed to embed the app
  in an iframe still need to be added.
* This alternative authentication cannot easily be scoped to a
  subset of endpoints without adding a lot of complexity, so it is
  accepted globally on the API for now.
2026-08-26 12:17:35 +02:00
lebaudantoine f1d3799434 🔖(minor) bump release to 1.29.0 v1.29.0 2026-08-25 23:22:29 +02:00
lebaudantoine e59aaaa998 📝(changelog) fix a minor changelog issue
Wrongly added to an old section during a rebase.
2026-08-25 23:14:16 +02:00
lebaudantoine 76a24d4787 ️(backend) replace blocking Redis KEYS with cursor-based SCAN
`cache.keys()` runs Redis `KEYS`, a full-keyspace scan on Redis's
single thread that blocks everything else, including session reads
in the same cache. Its cost scales with total keys, not matches,
and some managed providers disable `KEYS` entirely.

The trusted-lobby feature made this urgent: the waiting-list
endpoint scanned on every poll, and its polling audience grows from
a few admins to potentially every authenticated participant.

Switch to cursor-based `SCAN` via two `core.utils` helpers, deleting
in bounded batches so cleanup of a large room cannot block either.
A single seam also lets us forbid raw `cache.keys()` going forward.

`SCAN` still iterates the keyspace incrementally on the polled
path. If monitoring flags it, the follow-up is a per-room set
index — out of scope here since it changes the lobby storage model.
2026-08-25 22:48:47 +02:00
lebaudantoine 943b81676b (frontend) let authenticated users manage the lobby on trusted rooms
Frontend counterpart of the trusted-lobby backend feature: on
`trusted` rooms, any authenticated participant sees the waiting
notification and can accept or deny entry requests, not only admins
and owners.

Gating moves from role to capability: `useCanManageLobby` mirrors
the backend permission and derives from `useRoomData()`. Room
metadata is already synced into the query cache, so an access-level
change mid-meeting recomputes the capability on every client with no
new sync mechanism. It is only a UI gate; the backend re-checks
everything per request and fails closed.

Fetching moves into a single room-level `LobbyProvider`: the hook
was previously instantiated by two components and only worked
because React Query deduplicated their queries. The provider owns
one query and an explicit state machine - ways in (connection
established, ParticipantWaiting broadcast, panel opened, rights
regained while the panel is open) all arm and fetch; ways out
(rights lost, server 401/403) disarm and clear the cached list so
nothing stale can render.

Polling is tiered by audience since managers grow from a few admins
to potentially the whole room: 1s when acting (panel open),
10s when the notification is shown, and zero when the list is empty -
decided in the refetchInterval callback because structural sharing
suppresses data-keyed effects on identical empty responses. A quiet
room costs nothing; fetches are triggered by uncorrelated human
events, never synchronized across the room (the rights-regained
trigger is panel-gated for this reason).
2026-08-25 22:48:47 +02:00
lebaudantoine 7369379106 (backend) let any authenticated user manage the lobby on trusted rooms
On rooms with the `trusted` access level, any authenticated user
connected to the meeting can now manage the lobby. Requested by
several organizations, and a step toward generalized lobby
management once hubs and groups land (same organization only).

Being authenticated is not enough to grant the capability: a
`trusted` room means "trusted to join", not "trusted to decide who
else joins from outside the call". The new `CanManageLobby`
permission therefore also requires the requester to be currently
connected to the meeting, verified against LiveKit and failing
closed, like `IsPresentInMeeting`. The access level itself is never
cached and always read fresh, so an owner switching the room back to
`restricted` revokes the capability on the very next request - the
one guarantee we did not want to trade for performance.

Performance is traded elsewhere: the waiting list is polled by every
lobby manager, and on a trusted room that audience grows from a few
admins to potentially the whole meeting. Hitting LiveKit once per
poll per participant would not survive that fan-out, so presence is
memoized in Redis (`PresenceCache`, `PRESENCE_CACHE_TIMEOUT`, 1h).
Entries are created lazily because only the minority of participants
who actually manage a lobby ever need one, and only positive answers
are cached because a sticky negative would lock out someone joining
right after a miss for the whole TTL. Eager invalidation on
`participant_left`, `room_finished` and admin kick keeps the cache
honest; the TTL is the safety net when an event is lost, and its
value bounds how long a departed participant could still act.

Trade-offs in this v0:

* `PRESENCE_CLEAR_ON_PARTICIPANT_LEFT` gates the eager invalidation
  on `participant_left`: its cost is one Redis DELETE per departure,
  for every departure, so we want to be able to measure it in
  production and turn it off independently of the feature. When
  disabled, invalidation relies on `room_finished` and the TTL only,
  widening the stale window above.
* This can put non-trivial pressure on the cache at scale; the
  rollout will need to be monitored closely.
* The `participant_left` webhook must be enabled in the LiveKit
  deployment, otherwise eager invalidation silently degrades to the
  TTL-only behavior.
2026-08-25 22:48:47 +02:00
lebaudantoine c02d54b6ff ️(frontend) apply frugal constraint to the active meeting audio track
Apply the `VOICE_AUDIO_CONSTRAINTS` to the audio track used during
the active meeting to reduce bandwidth usage.

* Originally proposed by trummerschlunk to reduce bandwidth, but
  previously restricted to the join screen preview.
* Backport the standard voice constraints (48 kHz sample rate,
  mono channel, 16-bit sample size) to the active call session, as
  requested by the BBBA team.
2026-08-25 22:33:42 +02:00
Florent Chehab cec2eb5a10 (summary) add hostname to analytics properties
This helps track down what was the source of events.
This can be usefull when checking perf of different workers for instance.
2026-08-25 14:48:06 +02:00
snyk-bot 2858d141f4 ⬆️(frontend) upgrade posthog-js from 1.404.1 to 1.409.5
Snyk has created this PR to upgrade posthog-js from 1.404.1 to 1.409.5.

See this package in npm:
posthog-js

See this project in Snyk:
https://app.eu.snyk.io/org/lasuite-dinum-default/project/96ea03d8-8d09-493d-86bf-363f274e129e?utm_source=github&utm_medium=referral&page=upgrade-pr
2026-08-24 16:15:15 +02:00
snyk-bot f10429581b ⬆️(frontend) upgrade @pandacss/preset-panda from 1.11.3 to 1.12.0
Snyk has created this PR to upgrade @pandacss/preset-panda from 1.11.3 to 1.12.0.

See this package in npm:
@pandacss/preset-panda

See this project in Snyk:
https://app.eu.snyk.io/org/lasuite-dinum-default/project/96ea03d8-8d09-493d-86bf-363f274e129e?utm_source=github&utm_medium=referral&page=upgrade-pr
2026-08-24 15:58:10 +02:00
snyk-bot b3369cddf7 ⬆️(frontend) upgrade @tanstack/react-query from 5.101.1 to 5.101.4
Snyk has created this PR to upgrade @tanstack/react-query from 5.101.1 to 5.101.4.

See this package in npm:
@tanstack/react-query

See this project in Snyk:
https://app.eu.snyk.io/org/lasuite-dinum-default/project/96ea03d8-8d09-493d-86bf-363f274e129e?utm_source=github&utm_medium=referral&page=upgrade-pr
2026-08-24 15:50:19 +02:00
snyk-bot cfffadc780 ⬆️(frontend) upgrade i18next-resources-to-backend from 1.2.1 to 1.2.3
Snyk has created this PR to upgrade i18next-resources-to-backend from 1.2.1 to 1.2.3.

See this package in npm:
i18next-resources-to-backend

See this project in Snyk:
https://app.eu.snyk.io/org/lasuite-dinum-default/project/96ea03d8-8d09-493d-86bf-363f274e129e?utm_source=github&utm_medium=referral&page=upgrade-pr
2026-08-24 15:41:16 +02:00
snyk-bot 4ae31b3297 ⬆️(frontend) upgrade @fontsource-variable/atkinson-hyperlegible-next
Snyk has created this PR to upgrade @fontsource-variable/atkinson-hyperlegible-next from 5.2.6 to 5.3.0.

See this package in npm:
@fontsource-variable/atkinson-hyperlegible-next

See this project in Snyk:
https://app.eu.snyk.io/org/lasuite-dinum-default/project/96ea03d8-8d09-493d-86bf-363f274e129e?utm_source=github&utm_medium=referral&page=upgrade-pr
2026-08-24 15:20:49 +02:00
lebaudantoine 954991c7c3 📱(frontend) improve feedback screen responsiveness on mobile
Tighten the feedback screen layout on mobile viewports so it fits
comfortably on small phone screens and reads naturally without
horizontal scroll or cramped controls.
2026-08-24 14:51:07 +02:00
lebaudantoine 6e2a7f0ca6 📱(frontend) stack idle modal buttons in a column on mobile
Wrap the buttons in the idle modal so they stack vertically on
mobile viewports.

The horizontal layout was cramped on small screens; a column layout
gives each button a comfortable touch target and reads more
naturally on narrow displays.
2026-08-24 14:51:07 +02:00
lebaudantoine 24404e4ea2 📱(frontend) collapse mobile control bar items on narrow viewports
Apply the same approach as the PiP screen to the mobile control
bar: make sure it never overflows on small phone screens by
collapsing some controls into the overflow menu when there is not
enough horizontal room.
2026-08-24 14:51:07 +02:00
lebaudantoine f18d784615 🔖(minor) bump release to 1.28.0 v1.28.0 2026-08-24 11:32:06 +02:00
lebaudantoine 1bfe6b8977 🔇(frontend) suppress leaked WebSocket error events from livekit-client
livekit-client leaks the raw WebSocket error Event as an unhandled
rejection when the signal WebSocket errors after connect. This is
mostly seen on Firefox and coincides with signal reconnects.

The event carries zero diagnostic content — the close reason is
already logged by the SDK — so it only adds noise to error
tracking.

Filter it out from our reporting.

Upstream issue: https://github.com/livekit/client-sdk-js/issues/2062
2026-08-23 18:12:18 +02:00
lebaudantoine 84845709d0 🐛(frontend) make the device-in-use logic race-free
The device-in-use logic could race when multiple detection events
fired close together (e.g. rapid mic/cam toggles or several devices
becoming busy at once), leading to inconsistent store state.

Refactor the flow so all updates go through a serialized path,
removing the race conditions and keeping the in-use state
consistent regardless of event ordering.
2026-08-23 17:40:01 +02:00
lebaudantoine e000377b5c ♻️(frontend) track device-in-use state by id instead of by kind
Refactor the device-in-use handling to key state by device id
instead of only by kind (microphone or camera).

On computers with several devices of the same kind, keying by kind
meant that one device being in use marked the whole kind as busy,
even when the user could still use another device. Tracking per id
lets the UI and logic distinguish between devices correctly.

The store is updated accordingly so that per-device state is stored
and read consistently across the app.
2026-08-23 17:40:01 +02:00
lebaudantoine a9fa3eb4ba 🐛(frontend) treat "Timeout starting source" AbortError as device-in-use
Also consider `AbortError` with a message like "Timeout starting
video/audio source" as an "already in use" device error.

This can happen for other reasons in theory, but in practice most
occurrences are caused by another application still holding the
camera or microphone.

Route it through the existing device-in-use handling so users get
the same clear explanation as with the standard error, instead of a
generic failure.
2026-08-23 17:40:01 +02:00
lebaudantoine 2ec3f54532 🐛(frontend) handle Firefox/Windows AbortError on device start
Some versions of Firefox on Windows do not raise the
`NotReadableError` that LiveKit expects when the camera or
microphone fails to start. Instead, they surface an `AbortError`
with a message explaining the browser could not start the video or
microphone input.

This case was observed in PostHog error tracking and was not
handled, so users hit an unhelpful failure state.

Detect that specific `AbortError` and route it through the same
device-in-use / not-readable handling as the standard error, so
users get a clear explanation.
2026-08-23 17:40:01 +02:00
lebaudantoine d723c14d84 🐛(frontend) handle device-in-use errors on Chrome / Windows 10
On Chrome and Windows 10, when the camera or microphone is already
in use by another application, the browser rejects
`getUserMedia`. Without dedicated handling, users just saw their
camera or microphone not turning on, without any explanation.

Detect this specific failure and surface a clear message to the
user, so they understand another application is holding the device
and know what to do about it.
2026-08-23 17:40:01 +02:00
lebaudantoine e5f0b1c202 🐛(frontend) fix joined notification tile no longer rendering properly
The toast is enqueued on `ParticipantConnected`, which livekit-client
fires before the participant's track publications exist. At that
point `getTrackPublication(Camera)` is `undefined`, and the track
ref used by the toast is frozen that way — the toast never
re-subscribes to the participant.

Downstream effects:

* LiveKit's `useIsMuted` initializes to `false` when there is no
  publication, so the tile first paints with
  `data-lk-video-muted="false"`, which the stock CSS maps to
  placeholder `opacity: 0` — a blank box.
* Its effect then subscribes to `mutedObserver`, which defaults to
  `true` for the same situation, triggering a second render and
  only then starting the 200 ms fade-in. That render-effect-render
  transition is the visible lag behind the text.
* Because the track ref never updates, no `VideoTrack` ever mounts.
  When the camera is later subscribed, the placeholder fades out
  again, leaving the tile empty.

Re-derive the track ref inside the toast when the participant's
publications become available, so `useIsMuted` sees the real state
from the start and the tile renders the video (or a stable
placeholder) instead of a blank box.
2026-08-21 22:55:28 +02:00
lebaudantoine 9e0d57a8c6 🐛(frontend) hoist mute confirmation dialog to VideoConference level
`MuteButton` owned the confirmation dialog state locally. On the
participant tile, the button lives inside `FadeOverlay`, which
conditionally mounts its children based on tile mouse activity
(3 s idle timeout). Once the user moved the pointer onto the
react-aria portal, mouse events on the tile stopped, the idle timer
expired, `FadeOverlay` unmounted `MuteButton`, and the dialog was
destroyed with it — hence the "closes randomly" behavior.

Render the dialog once at the VideoConference level via a shared
`MuteAlertDialogProvider`, so its lifetime is independent from any
button that opens it.
2026-08-21 22:55:28 +02:00
lebaudantoine 7ba0803b71 💄(frontend) increase the blur intensity
Bump the blur strength applied to virtual backgrounds so it obscures
the background more effectively.

Requested by users.
2026-08-21 22:55:28 +02:00
lebaudantoine beacfc3d3f 🐛(frontend) stop init_virtual_background from firing on blur updates
A regression introduced with the custom virtual background feature
(16daf7b8, March 26): every blur init on Firefox, and every update
call, was going through `init_virtual_background`, which raised an
error because there is no virtual background to initialize in that
code path.

The existing guard was incorrect. Replace it with an early return,
so `init_virtual_background` is only executed when there is actually
a virtual background to initialize.

This does not fix the other virtual background issues that currently
prevent some users from activating their camera; those will be
addressed separately.
2026-08-21 22:55:28 +02:00
lebaudantoine 52e5d99e83 📈(frontend) track errors when starting or stopping a recording
Send an analytics event whenever starting or stopping a recording
fails, so we can monitor how often it happens.

It also gives support the context needed to understand what went
wrong when inspecting a user's session.
2026-08-21 22:55:28 +02:00
lebaudantoine 15ca2b41b4 🐛(frontend) use state instead of a ref for MoreControls container
After the recent refactoring, `MoreControls` only renders once. On
that first render, the container ref is `null`, and when it later
gets a reference to the div, the ref update does not trigger a
re-render.

As a result, the additional controls were never showing up.

Switch from a ref to a state to track the container element. State
updates do trigger a re-render, so the controls now show as
expected once the container is available.
2026-08-21 22:55:28 +02:00
lebaudantoine e34f3dd219 🐛(frontend) treat client-initiated connect aborts as events
`connect()` calls can be aborted by a `disconnect()` before the
join completes — typically when the user leaves, refreshes,
navigates away early, or when the component remounts. This is
expected behavior, not a failure.

Stop reporting these as errors. Track them as analytics events
instead, so we can still monitor their volume without polluting
error dashboards.

Volume is relatively low, but keeping visibility helps troubleshoot
users who complain about difficulty connecting to a room, and could
also flag a component that is re-rendering and killing the
connection.
2026-08-21 22:55:28 +02:00
lebaudantoine feb573551f 💡(frontend) warn in vite config on MediaPipe asset/track-processor drift
Add a build-time warning in the Vite config that fires when the
MediaPipe assets copied by `vite-plugin-static-copy` fall out of
sync with the version used by `@livekit/track-processors`.

This catches at build time the kind of drift that would otherwise
only surface at runtime as broken virtual backgrounds.
2026-08-21 18:09:31 +02:00
lebaudantoine 1d0a0cc637 ⬆️(mail) upgrade Node image for mails-install to align with the frontend
Node 18 is now legacy, and running `make mails-install` was failing
with a "bad engine" error against the current image.

Bump the Node image used for `mails-install` to a more recent
version, aligned with the one used by the frontend, so the make
target runs cleanly again.

Note: the Alpine variant cannot be used here, as `bash` is required
to build the mail templates.
2026-08-20 16:09:03 +02:00
snyk-bot eae93f771f 🔒️(mail) fix vulnerabilities in src/mail/package.json
Upgrade dependencies to fix the following reported vulnerability:

* https://snyk.io/vuln/SNYK-JS-DEEPMERGETS-18912249
2026-08-20 16:09:03 +02:00
Florent Chehab 45175a2a54 ♻️(summary) uniformized S3 settings case
Quick commit to uniformize the S3 related settings case.
2026-08-20 12:50:26 +02:00
Florent Chehab 8b22059b18 (summary) configurable s3 region
When using other S3 providers than minio,
We neeed to configure the region
2026-08-20 12:41:48 +02:00
lebaudantoine 87dbd8069d ⬆️(frontend) upgrade livekit-client and @livekit/components-react
Bump livekit-client 2.20.0 -> 2.21.0 and
@livekit/components-react 2.9.21 -> 2.9.23. Patch/minor upgrade
with data streams v2, data channel flow control and reliability
fixes, iOS Safari double-playback fix, and a build-time type
resolution fix for `skipLibCheck: false`.

No breaking API changes.
2026-08-20 11:08:27 +02:00
lebaudantoine c7420c59a3 ⬆️(frontend) upgrade posthog-js from 1.395.0 to 1.404.1
Minor version upgrade with bug fixes and internal improvements. No
breaking changes, API deprecations or required actions on our side.

Highlights:

* Better handling of `sendBeacon` quota rejections so events are no
  longer silently dropped.
* Improved error catching for synchronous throws from a
  monkey-patched `window.fetch`.
* Source map improvements for easier debugging.
2026-08-20 11:08:27 +02:00
lebaudantoine f46babfcbd ⬆️(frontend) upgrade i18next and react-i18next patch versions
Bump i18next from 26.3.1 to 26.3.6 and react-i18next from 17.0.8 to
17.0.10. Both are patch upgrades with bug fixes and no breaking
changes, requiring no code modifications on our side.

i18next:

* 26.3.6: widen the optional TypeScript peer dependency range to
  include v7.
* 26.3.1: fix a regression in type definitions related to
  `keyPrefix` to prevent incorrect type pollution.

react-i18next:

* 17.0.10: improve warning messages for `useTranslation` and
  `Trans` to help debug common issues, especially in monorepos.
  Add a new development-only warning when `useTranslation`
  suspends while loading translations.
* 17.0.9: add TypeScript 7 support and fix related type-checking
  issues with the `<Trans>` component.
2026-08-20 11:08:27 +02:00
lebaudantoine 7c465f2148 ⬆️(backend) bump sqlparse from 0.5.5 to 0.6.0
Update sqlparse to 0.6.0 to pick up fixes for the following high
severity CVEs, all reported against versions prior to 0.6.0:

* CVE-2026-54284: https://avd.aquasec.com/nvd/cve-2026-54284
* CVE-2026-59893: https://avd.aquasec.com/nvd/cve-2026-59893
* CVE-2026-71491: https://avd.aquasec.com/nvd/cve-2026-71491
2026-08-19 13:30:25 +02:00
lebaudantoine d005f202c6 (backend) accept form-urlencoded on the user token endpoint
Accept `application/x-www-form-urlencoded` requests on the user
token endpoint, in addition to the existing JSON support.

This aligns the endpoint with the OAuth 2.0 specification for token
endpoint requests (RFC 6749, sections 3.2 and 4.4.2), so standard
OAuth 2.0 client libraries can call it without any customization,
while keeping backward compatibility with existing JSON clients.
2026-08-19 13:30:25 +02:00
chaitanyaphatak a82023f8b0 📝(docs) fix minor typos in comments and docstrings 2026-08-17 10:30:16 +02:00