Compare commits

...

27 Commits

Author SHA1 Message Date
Robin
9f727b904b Remove patched version of WebCodecs types
The patch has now been upstreamed to @types/dom-webcodecs.
2025-05-09 10:48:09 -04:00
Quentin Gliech
380880137e Keep the HTML lang attribute in sync with the i18next language (#3247) 2025-05-05 17:12:00 +02:00
fkwp
8828b705d9 Merge pull request #3235 from element-hq/fkwp/refactor_dev_backend
DevX: Properly server .well-known and use endpoint routing as described in self-hosting.md
2025-05-05 16:30:02 +02:00
fkwp
b7e5b81dbf update yarn.lock to remove plugin-basic-ssl 2025-05-05 16:25:18 +02:00
fkwp
e12dfa9a9f prettier 2025-05-05 16:17:16 +02:00
fkwp
48b4fd63fe prettier 2025-05-05 16:09:34 +02:00
fkwp
f2b68e6dee add localhost domain to certificate 2025-05-05 16:02:19 +02:00
fkwp
128851263b use ssl cert from mini localhost CA 2025-05-05 16:01:58 +02:00
fkwp
679ff8c2af adapt to TLS setup 2025-05-05 15:47:03 +02:00
fkwp
9dcaa60982 added app.m.localhost for element web 2025-05-05 15:26:47 +02:00
fkwp
34a223f04b add yarn dev --host hint 2025-05-05 14:54:14 +02:00
fkwp
005402d8d1 prettier 2025-05-05 14:48:45 +02:00
fkwp
76fb16dbfd update README.md to reflect TLS settings 2025-05-05 14:44:21 +02:00
fkwp
1a3ac826b2 Merge branch 'livekit' into fkwp/refactor_dev_backend 2025-05-05 14:22:10 +02:00
fkwp
25afbf4303 base playwright backend on dev-backend 2025-05-05 14:20:51 +02:00
fkwp
9c65b40225 adapt new hostname schema and move to https URLs 2025-05-05 13:07:37 +02:00
fkwp
fb63e64eb4 removed old localhost tls certificates 2025-05-05 13:06:58 +02:00
fkwp
8679b90bd7 add new certs to nginx section 2025-05-05 13:06:02 +02:00
fkwp
14ff6dce93 localhost TLS mini CA including wildcard certs for *.m.localhost 2025-05-05 13:05:07 +02:00
fkwp
08f034251c Add call.m.localhost pointing to yarn dev --host 2025-05-05 13:02:58 +02:00
fkwp
06e1a4eb1e Adapt new naming schema *.m.localhost 2025-05-05 12:57:23 +02:00
Timo
aee30d8402 Fix release notes (#3244) 2025-05-02 11:49:49 +02:00
fkwp
9416c41d62 added note about certificate exceptions 2025-04-25 00:18:27 +02:00
fkwp
b61bed0197 cleanup used ports, reflect endpoint routing of JWT service and SFU websocket connection 2025-04-25 00:18:27 +02:00
fkwp
8ad1d60975 remove NOTE wrt. legacy livekit key from README.md 2025-04-25 00:18:19 +02:00
fkwp
1ae876de26 remove legacy livekit key 2025-04-25 00:18:19 +02:00
fkwp
09177b90d3 added matrix-rtc server to route jwt and sfu traffic. renamed file to dev_nginx.conf 2025-04-25 00:18:01 +02:00
25 changed files with 370 additions and 1118 deletions

View File

@@ -192,11 +192,6 @@ To use it, create a local config by, e.g.,
The `config.devenv.json` config should work with the backend development
environment as outlined in the next section out of box.
> [!NOTE]
> Be aware, that this `config.devenv.json` is exposing a deprecated fallback
> LiveKit config key. If the homeserver advertises SFU backend via
> `.well-known/matrix/client` this has precedence.
You're now ready to launch the development server:
```sh
@@ -212,12 +207,20 @@ See also:
A docker compose file `dev-backend-docker-compose.yml` is provided to start the
whole stack of components which is required for a local development environment:
- Minimum Synapse Setup (servername: `synapse.localhost`)
- LiveKit JWT Service (Note requires Federation API and hence a TLS reverse proxy)
- Minimum TLS reverse proxy (servername: `synapse.localhost`) Note certificates
are valid for at least 10 years from now
- Minimum Synapse Setup (servername: `synapse.m.localhost`)
- LiveKit Authorization Service (Note requires Federation API and hence a TLS reverse proxy)
- Minimum LiveKit SFU Setup using dev defaults for config
- Redis db for completeness
- Minimum `localhost` Certificate Authority (CA) for Transport Layer Security (TLS)
- Hostnames: `m.localhost`, `*.m.localhost`
- Add [./backend/dev_tls_local-ca.crt](./backend/dev_tls_local-ca.crt) to your web browsers trusted
certificates
- Minimum TLS reverse proxy for
- Synapse homeserver: `synapse.m.localhost`
- MatrixRTC backend: `matrix-rtc.m.localhost`
- Local Element Call development `call.m.localhost` via `yarn dev --host `
- Element Web `app.m.localhost`
- Note certificates will expire on Thu, 03 May 2035 10:32:02 GMT
These use a test 'secret' published in this repository, so this must be used
only for local development and **_never be exposed to the public Internet._**
@@ -230,6 +233,16 @@ yarn backend
# podman-compose -f dev-backend-docker-compose.yml up
```
> [!NOTE]
> To ensure your local development frontend functions properly, youll need to
> add certificate exceptions in your browser for `https://localhost:3000`,
> `https://matrix-rtc.m.localhost/livekit/jwt/healthz` and
> `https://synapse.m.localhost/.well-known/matrix/client`. This can be either
> done by adding the minimum localhost CA
> ([./backend/dev_tls_local-ca.crt](./backend/dev_tls_local-ca.crt)) to your web
> browsers trusted certificates or by simply copying and pasting each URL into
> your browsers address bar and follow the prompts to add the exception.
### Playwright tests
Our Playwright tests run automatically as part of our CI along with our other

View File

@@ -1,5 +1,5 @@
server_name: "synapse.localhost"
public_baseurl: http://synapse.localhost:8008/
server_name: "synapse.m.localhost"
public_baseurl: https://synapse.m.localhost/
pid_file: /data/homeserver.pid

155
backend/dev_nginx.conf Normal file
View File

@@ -0,0 +1,155 @@
# Synapse reverse proxy including .well-known/matrix/client
server {
listen 80;
listen [::]:80;
listen 443 ssl;
listen 8448 ssl;
listen [::]:443 ssl;
listen [::]:8448 ssl;
server_name synapse.m.localhost;
ssl_certificate /root/ssl/cert.pem;
ssl_certificate_key /root/ssl/key.pem;
# well-known config adding rtc_foci backend
# Note well-known is currently not effective due to:
# https://spec.matrix.org/v1.12/client-server-api/#well-known-uri the spec
# says it must be at https://$server_name/... (implied port 443) Hence, we
# currently rely for local development environment on deprecated config.json
# setting for livekit_service_url
location /.well-known/matrix/client {
add_header Access-Control-Allow-Origin *;
return 200 '{"m.homeserver": {"base_url": "https://synapse.m.localhost"}, "org.matrix.msc4143.rtc_foci": [{"type": "livekit", "livekit_service_url": "https://matrix-rtc.m.localhost/livekit/jwt"}]}';
default_type application/json;
}
# Reverse proxy for Matrix Synapse Homeserver
# This is also required for development environment.
# Reason: the lk-jwt-service uses the federation API for the openid token
# verification, which requires TLS
location / {
proxy_pass "http://homeserver:8008";
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
error_page 500 502 503 504 /50x.html;
}
# MatrixRTC reverse proxy
# - MatrixRTC Authorization Service
# - LiveKit SFU websocket signaling connection
server {
listen 80;
listen [::]:80;
listen 443 ssl;
listen [::]:443 ssl;
listen 8448 ssl;
listen [::]:8448 ssl;
server_name matrix-rtc.m.localhost;
ssl_certificate /root/ssl/cert.pem;
ssl_certificate_key /root/ssl/key.pem;
location ^~ /livekit/jwt/ {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# JWT Service running at port 8080
proxy_pass http://auth-server:8080/;
}
location ^~ /livekit/sfu/ {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_send_timeout 120;
proxy_read_timeout 120;
proxy_buffering off;
proxy_set_header Accept-Encoding gzip;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# LiveKit SFU websocket connection running at port 7880
proxy_pass http://livekit-sfu:7880/;
}
error_page 500 502 503 504 /50x.html;
}
# Convenience reverse proxy for the call.m.localhost domain to yarn dev --host
server {
listen 80;
listen [::]:80;
server_name call.m.localhost;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name call.m.localhost;
ssl_certificate /root/ssl/cert.pem;
ssl_certificate_key /root/ssl/key.pem;
location ^~ / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass https://host.docker.internal:3000;
proxy_ssl_verify off;
}
error_page 500 502 503 504 /50x.html;
}
# Convenience reverse proxy app.m.localhost for element web
server {
listen 80;
listen [::]:80;
server_name app.m.localhost;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name app.m.localhost;
ssl_certificate /root/ssl/cert.pem;
ssl_certificate_key /root/ssl/key.pem;
location ^~ / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://element-web:81;
proxy_ssl_verify off;
}
error_page 500 502 503 504 /50x.html;
}

View File

@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDGjCCAgKgAwIBAgIUGdiFHhH4KL2pqBjMQHQ+PVIkSV8wDQYJKoZIhvcNAQEL
BQAwHjEcMBoGA1UEAwwTRWxlbWVudCBDYWxsIERldiBDQTAeFw0yNTA1MDUxMDMy
MDJaFw0zNTA1MDMxMDMyMDJaMB4xHDAaBgNVBAMME0VsZW1lbnQgQ2FsbCBEZXYg
Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDA2y0hjmNn1vRsVSdy
8IOfo8N1q9UgkhQWpGKXzPh+D5d1fnuJEmHIVwtDEtS/PwQ43LTmegChPtKH9jdT
tG0IihW9Ja5YNG+9xAwaoA/sB3CGCBYsz+2/XjVUpXoBJXIPoFBWsn+K0oeFw9fw
eRO1z9abM4cl+LjKzMNM8CCyu9uI1MaGjYez2YIWvG854VucLxX7HSlMJxZNWnie
Ui7fMakuJhB2+aiIQjdKxy4E5RHNhzYG/LXhvP+wBYBDPNRsP3rtzEaE9HAveL9K
FGqd3R4cBia6r1WIXmpAzyu5RGP5Eou0TZlGkal96/bF0I7q/pKlL23Jt1BLPiQU
KGKrAgMBAAGjUDBOMB0GA1UdDgQWBBQJqBjMu61c1p24txw/y+kv3D+V6DAfBgNV
HSMEGDAWgBQJqBjMu61c1p24txw/y+kv3D+V6DAMBgNVHRMEBTADAQH/MA0GCSqG
SIb3DQEBCwUAA4IBAQB8m2YfFGLugNt5vAAOvNxVqDA8c72yCVYr3CBCpmTIEY5Z
d3qVGhG9//ux6+J8ntkSwd9nV5GJyYXHukCG1VavnAWolWdNF/WAllf0jhLuz7kD
/cJnuI1By4tBsBmSz851i6HJ4t5k99Be+6GQVzi0e7zzfxTHZE4xP2J6Ox8QbPsP
n0m76nIp/WbWaJqzvIIjJhmUUPPv+4wN+eOArgjiGLzptM2qTtGZtd0c9nS5gvep
+mEbSUN9zkhAroZf80wf+hEvy+fJ94VbZ9QjTzTg7odZLrsXGIe8DaG63EYRQ25b
W5iYBAreln5fGSt7qHsGfqwZibTEk/Lx3dydO1Kg
-----END CERTIFICATE-----

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDA2y0hjmNn1vRs
VSdy8IOfo8N1q9UgkhQWpGKXzPh+D5d1fnuJEmHIVwtDEtS/PwQ43LTmegChPtKH
9jdTtG0IihW9Ja5YNG+9xAwaoA/sB3CGCBYsz+2/XjVUpXoBJXIPoFBWsn+K0oeF
w9fweRO1z9abM4cl+LjKzMNM8CCyu9uI1MaGjYez2YIWvG854VucLxX7HSlMJxZN
WnieUi7fMakuJhB2+aiIQjdKxy4E5RHNhzYG/LXhvP+wBYBDPNRsP3rtzEaE9HAv
eL9KFGqd3R4cBia6r1WIXmpAzyu5RGP5Eou0TZlGkal96/bF0I7q/pKlL23Jt1BL
PiQUKGKrAgMBAAECggEAAPX2kxi5AQ7ul82SzT1KgpSXyDHLdYaUyAoYnaX9RO+B
8ylmpyeqygs4+KQS4EMJm9jpo85Oy37bIKdG3kljU6wQcKlL5Y+ZUOo1nzpV6fid
hGVs6ts8VXw8KshKQ9AyccZ8L/pirUfgOffgTwfjY7/90zceAL/s98GuZWc62nkX
55joQv/OikqYfAGP/U6Bp2Zyf23DwJB09Z3B6NnZj/ZyAbDrDEHuA15LhCOcCczp
IU/mFEywBPHT9Tg4w4Beq78PeAETvku2UalYRLhP3RLlXr2oEbwUtINRVt2QjZ85
Esps4uCqL/mgQluIebtudD9HL/YMlNPXue1mDXFxJQKBgQDgZZY4yJBcf488T1V6
HNm06b/LvVGj253pKgw14hpY1xQu3Ymgzv1GEqzhSYdzxhpmj0tMUNHxAp+YdGQu
SZ0wcPKhw0aYVkIjDRYDC3Wn5GJhyIEYHGYMo/n4l49UzHRBPOTDzp49DkHTKBgh
XgIIazYT3CkjTIMRrkUv+qfIPQKBgQDcBGu/mqbjxs4sN3zqPS4aB21o6t6W0sXs
ZP9w6RlTPQi5U2oRbftjZtYc0bbEgkMUImB1HwYPQT5pJ+MyC414xDvSc2exBr5d
To6yyPIy78Tf5PHM12fpKV92nSvoz/pSjYcGxxDtKfPqu+t8mOJfjCV1lLLA+xuB
DDaE4p8dBwKBgQCdAne6A5v/HMH8UQZeCxHJpESvKiiVnnU/UEx651nID7XvlNNX
0X0mKqsMd4ZvW43ddSYan/JF0LAa3FW8jYWO/3jF9vzOWoysOdvNBZetgf/Uq5ao
aDZ/YbzmVCXWD7jIbPMkjs3pqrAkL0mzDzQc7+dGviWKrV6IYIfIqnn7gQKBgDCz
vdIk/qpO+JZrFfiX4Fucp0hhLTJ/p5ZDaRPqVVPKn+K+Jy2ChfIj8mNgvK9VEloj
nexvGJ1J2PHYBX+vdPp1nbRhHWPfVUY8PHQw7QP/dToGaMvqJrNDGEGeWvjnCMc7
UtdaO1H0Rm0AegkTopB56lTTvJnhO95eALd7nrMDAoGAEPdzJtWoKafp49svhSj0
hiXQv2SPBwVUN4LZ4SOWiXUcmYYm80aNpYKLkBxYjrfqFWhE7NUHLGp8YorQWKY2
acD9AReHk/xku0ABy6jeYmSCmCxASxst5liKD+l12sk0gB0rk5MBxB4Uu1MIbQZ2
aCASX3AVD2/XyC2MKkzc8Eg=
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDZzCCAk+gAwIBAgIUXizLjwkdqepX0bh0K3abeJxj68IwDQYJKoZIhvcNAQEL
BQAwHjEcMBoGA1UEAwwTRWxlbWVudCBDYWxsIERldiBDQTAeFw0yNTA1MDUxMzU5
MTFaFw0zNTA1MDMxMzU5MTFaMBgxFjAUBgNVBAMMDSoubS5sb2NhbGhvc3QwggEi
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrzGSScSgaQuZdELGFYiLiYRwr
LKyUdNr0rsPcOo0bvbeZ3zQMeUMRNlA69zGFdarumiDRXUoAmZI39WmH95aX3d+A
U7EFnWev7xpWSVhSYj8T0d4rke8HjGk3LpaffJ93tbJuagBIH1ouuN6AOdzWs8hp
RYIomWleEeeuVnnfaMwaXOdc+ihJJ6wzm2hwQSfdpjZPWBDd/DFft1ZXxIZOCjDs
rEIiI7uU8iZPLB3QEM/tgxSSAOxrcKvQvxZokk+FD7aMJFP71IfieLCEzMTP1VXa
tP7UTAKAqB2NyDJ8m3IHbOINiqcdFvFR3R1D9bXOYE4oRynNvYZrQUGnL2RtAgMB
AAGjgaIwgZ8wHwYDVR0jBBgwFoAUCagYzLutXNaduLccP8vpL9w/legwCQYDVR0T
BAIwADALBgNVHQ8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwMAYDVR0RBCkw
J4IJbG9jYWxob3N0ggttLmxvY2FsaG9zdIINKi5tLmxvY2FsaG9zdDAdBgNVHQ4E
FgQUfdh1p52ZgWyZcBgBXGwKi4EnUE0wDQYJKoZIhvcNAQELBQADggEBAKrHEuB6
33j8+EwSHw3zrvt/DRXK2BDHI1Ir9JcztSunaKAjZXVvf/dvZp0Xs1dEdJIdnv6G
iZYhBbOqDqpQZbf2h/h0kuu5yZSBUdnQXnYNxlhp2UaC/UEgw5iZT/p1rm7RjVie
y4Dp2WytV5iZOLmLj6xDvd3DXazgJPWIRX8p8qJZbKTkwCjTr7nDIj8jjG1sVFf7
1RJBO5/6WSnImrpDmlLUrvjiKvbxcdseDJyBOhTwdRdSk4S2M+s5tR5j2I1gXLOq
J5ioN76+SCrTY0K0WKRy9oOXWO1/X3+VYcekp+0F3SGkd5w17jylCv1XIGHAdEsQ
v2z2/aMI/7sAD2Q=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCrzGSScSgaQuZd
ELGFYiLiYRwrLKyUdNr0rsPcOo0bvbeZ3zQMeUMRNlA69zGFdarumiDRXUoAmZI3
9WmH95aX3d+AU7EFnWev7xpWSVhSYj8T0d4rke8HjGk3LpaffJ93tbJuagBIH1ou
uN6AOdzWs8hpRYIomWleEeeuVnnfaMwaXOdc+ihJJ6wzm2hwQSfdpjZPWBDd/DFf
t1ZXxIZOCjDsrEIiI7uU8iZPLB3QEM/tgxSSAOxrcKvQvxZokk+FD7aMJFP71Ifi
eLCEzMTP1VXatP7UTAKAqB2NyDJ8m3IHbOINiqcdFvFR3R1D9bXOYE4oRynNvYZr
QUGnL2RtAgMBAAECggEAJaFQii8U/KOYt9vXNoMnZvSkaeSQLLhn2V6Kciu1CtWE
aMTWLsFE6nk+G5xXkYcTmM3T0GghtH3u5CjyI6EcsEkeEorCZJt0wbmayDmqiekR
LfMzOdHuTHX5+edPgMGYYG1BFyRKyYFsjH1b5zRFZhXdGQnrl5760GsVlz9D1KZQ
iHcT+q1S2tmZeoUukQnADENKXUMCyTGM5FCddgNtsWnGDsTDayh7hUdvDkB+mW4G
lSp+BZuc3PCwpbD6qkXvfugWs6CUAAtXoV3ceWgxQ+TEnNlwxaG1AyugfgNUBolk
8xgeZt4r5QId03jsHDf7hpBAofcaCd5EMIIQYFvWoQKBgQDlbAvAzEFPTZZn2nRV
Xagw4xjqVc1LLEKLCWq0N5rEkwn0h90Dz5N7/3NuonP/sIDsDHCbyiOYBI1Ck6Xi
0WuB+OyKDh+xeF2mekN9G9ywPahdK5lT/TVsxXFyZlwtVv1x/6KBO4yv5URizxqU
gyAPDDxfD/KcNjkOBaodWEwQGQKBgQC/s2gPDBtQkjLwkHXchBomLww5eLlVrac1
WK4UX6uSdOgrjJ375OOgMTxe8NVZdOuAKytGXRWDwgH3nVWvuZhe7dGlX3JMuSer
e9VwDpBESrvqcR4ruL6wm8wej6BXyjH0wD3FHb0S5HfuBDxTn+4bDwrbRzOUMNgy
lSppuflxdQKBgQDiZcIfazFT8evn5nMAvuC4BZNTxIJHmZC9JfjPiUPIkpWzYtOe
7BvNtKOT3Op9uw8uYYRKqKqBXJSNy6ha8XCXHS9HeXKbLn20SFkLQBCDNwVLlDfF
40zyXtF6JDr4XyzSb4NM5pgKCER5AYloXxGm59s3sEQpFXUuOjbKqJS/GQKBgAoI
c7vF4HAZFr1sch62cz/oWnVvkhOf4Q5zs7ixQSOLJtOQqnwSgK9TpFs7s47ZBbJR
kBRAru2Ua9Hv1Bo8VnMxczV6h1roneDlvEf/GyHX33nnrbKQGrrXjJlU3wl5NaAf
p5v3cHvapUQ5yIZ/6lBUOzc6xMJOxCHxmKSr7Rg5AoGAbEE4lt6Xh2dnBPJ81eNI
IDrw/3ITY53qAY4Bx88CByIFuu8CEUdUZprh98jSl6ic1tMinZfUhRMwABLrUD51
DGst8iGLPD9u83iMcUHI/L+p7AbxrKLvWXZrF5UZm440c9mSWqfhPaTBosPtNDsG
LfETwH1flKXMTXd2xA9RTE4=
-----END PRIVATE KEY-----

38
backend/dev_tls_setup Normal file
View File

@@ -0,0 +1,38 @@
#!/bin/bash
# Step 1: Create a Root CA key and cert
openssl genrsa -out dev_tls_local-ca.key 2048
openssl req -x509 -new -nodes \
-days 3650 \
-subj "/CN=Element Call Dev CA" \
-key dev_tls_local-ca.key \
-out dev_tls_local-ca.crt \
-sha256 -addext "basicConstraints=CA:TRUE"
# Step 2: Create a private key and CSR for *.m.localhost
openssl req -new -nodes -newkey rsa:2048 \
-keyout dev_tls_m.localhost.key \
-out dev_tls_m.localhost.csr \
-subj "/CN=*.m.localhost"
# Step 3: Sign the CSR with your CA
openssl x509 \
-req -in dev_tls_m.localhost.csr \
-CA dev_tls_local-ca.crt -CAkey dev_tls_local-ca.key \
-CAcreateserial \
-out dev_tls_m.localhost.crt \
-days 3650 \
-sha256 \
-extfile <( cat <<EOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
DNS.2 = m.localhost
DNS.3 = *.m.localhost
EOF
)

View File

@@ -1,8 +1,8 @@
{
"default_server_config": {
"m.homeserver": {
"base_url": "http://synapse.localhost:8008",
"server_name": "synapse.localhost"
"base_url": "https://synapse.m.localhost",
"server_name": "synapse.m.localhost"
}
},
"disable_custom_urls": false,

View File

@@ -1,5 +1,5 @@
server_name: "synapse.localhost"
public_baseurl: http://synapse.localhost:8008/
server_name: "synapse.m.localhost"
public_baseurl: https://synapse.m.localhost/
pid_file: /data/homeserver.pid

View File

@@ -1,22 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDtzCCAp+gAwIBAgIUCmJjl3HAeLmrPwRg+/OzikW6peQwDQYJKoZIhvcNAQEL
BQAwazELMAkGA1UEBhMCR0IxDzANBgNVBAgMBkxvbmRvbjEPMA0GA1UEBwwGTG9u
ZG9uMQ4wDAYDVQQKDAVBbHJvczEWMBQGA1UECwwNSVQgRGVwYXJ0bWVudDESMBAG
A1UEAwwJbG9jYWxob3N0MB4XDTI0MTEwNDIxNDcwMFoXDTM0MTEwMjIxNDcwMFow
azELMAkGA1UEBhMCR0IxDzANBgNVBAgMBkxvbmRvbjEPMA0GA1UEBwwGTG9uZG9u
MQ4wDAYDVQQKDAVBbHJvczEWMBQGA1UECwwNSVQgRGVwYXJ0bWVudDESMBAGA1UE
AwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs368
ExLSudP8luNoY5UfaPqBSVJUPYBi+JGyd36tyN75p5OI7xSfHTttQxuD4KrExBFP
C8mAhE1eoZPBVBOZJ4FYWBJfMaQnCjeqU+laP36td65kSJYbUYlKYH1WpxEpCdgx
wWOKkP/kPX5YXbYqODx9aBJXgoT3yAJW7AniIoL+eLFnS9Xo86TPqCDBTJU9ocwK
gPIDLhDv60724rhZT1kbGp7ECqRovndoDTQjuws2D3yNMfQ+4rrQGPXHGmP5PcaR
0R7uueB+6APyC7MJbuhbxxg/+DFHrRi3lJsgwxuh2hi/+vWw8zgKlgYIwHFA9X0l
cX0UlQdENMH3bgcGIwIDAQABo1MwUTAdBgNVHQ4EFgQUUFGxw7zoiHXGwRqtagjZ
RPYc85cwHwYDVR0jBBgwFoAUUFGxw7zoiHXGwRqtagjZRPYc85cwDwYDVR0TAQH/
BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEALokb1z2lu3qW141b2wm14ilZQKCZ
reNNuUR95Uom96FXPH4QVEH+mYTXXJ5UrfNhQYKQFpdE+5S4HL/UqEOxtWvbAHpK
nsLQ62J8m+0+uwiJGqeQpWr03KJgXDAVE9X3XwMlp/+buxSLhc+GIHWuXW56itV2
jiZJYjhO5SnhhgTWNoVZk93qXuuWEN0yacw7c3Fr1IvFYYYWufbXTk70dbZihPDK
VD141o8tpp6FerSKHNYDqkVFDyTz3DVOhQQJ59zfMre7bFr+PpTTl4vIuGzXEY+E
HPjUSlOzwkCoh5fu7Fs3qG55rJt8akhTEoKpiBTaLucgAjVWNHeci1+Yxg==
-----END CERTIFICATE-----

View File

@@ -1,28 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCzfrwTEtK50/yW
42hjlR9o+oFJUlQ9gGL4kbJ3fq3I3vmnk4jvFJ8dO21DG4PgqsTEEU8LyYCETV6h
k8FUE5kngVhYEl8xpCcKN6pT6Vo/fq13rmRIlhtRiUpgfVanESkJ2DHBY4qQ/+Q9
flhdtio4PH1oEleChPfIAlbsCeIigv54sWdL1ejzpM+oIMFMlT2hzAqA8gMuEO/r
TvbiuFlPWRsansQKpGi+d2gNNCO7CzYPfI0x9D7iutAY9ccaY/k9xpHRHu654H7o
A/ILswlu6FvHGD/4MUetGLeUmyDDG6HaGL/69bDzOAqWBgjAcUD1fSVxfRSVB0Q0
wfduBwYjAgMBAAECggEACTqdSExxzJ+LX5ARFaWyOBSWly2GKqSyR14+aInOklhx
9QgkmfOxJrCf3TvJ8RWhXloW0Aqr8qGDxG0Ixgjn7rG7gskXCey1xn8MNppLS0kj
ztaG+NB3AR89ABm8XdoHsSY45geh3/Ni9I0i1VardGQafUJhgNLTZqjwIodzkBtJ
S/bi4uFk1lGNfuvWQvWqzGXUvd1l1YupV6iA4GfhXlUvrSBZwftLBD6xEvQaSqsA
pHvBxTfMXG4RMAkNPDIElkuQ8++CGi1gIRkJfmrv4OgbbitteMnxqqqGYV0zSNCg
R/5FG6umIV7lDLBHZCSCk7wmfmq2UUvzhHThHy4yMQKBgQDu4TwFJCIcVIj7Wj4r
DUBFvz6Lgbltqb+YAMUBtpiDcAQxDJWmedh6dK04ts5CFAFRlRjjuz2uFn7qlVBm
uye9R7tL+tOv5viqDXU78a4snFywoXub6yzpbxrW8B4W1pdIUvQmhwCcDwvO1V24
7Vj2vxcM5I9dsk1aCQSi3VY5yQKBgQDAW/VoTRwhU6OUc6sji5Z5dnkMjkP6NZK9
CSrTWLAMGaLPY+g6fFS7JMNSvfWm/okypD6rcN7p0cxMK3mfFKmMiyPRde0wdrci
sGFjGxM/2d2D7KTMC9iMYwA0K17UIna+UiYPfhR/muIg/dCyjlkKDFs9Z4jk//r1
91bmznt2iwKBgFdiYXhn/Wprqih4nKFXGZnqGdEixVhObl4GegrkZuo+AeqHdf8O
N5ikMfG7PbyCYPEdH5u/FRMn+4mI0X6jHChroyJqQSHp1jEu9yHUiSicknOyvusM
nsNN932FHRyxp2m3nsSxQhHUlzc0ajKJ8K9iu+XlfmSCIzW6cs25Nh+xAoGBAJro
M0wIdPPdsCj3sUVRvx8XqknTM6kGhaIYBNXoYPWNm5BaC4U15OJEq8sxUOdnqcMP
g6x6m/k+S8C3bh0O/a9Bydl/l0BlCfw0gGjYP/s2ju4Tn272xy/e9iYNGzPIgUmp
TB9D0GwmpZ4d6HgyrD+sTbm4bATGpCp6QhBjDggbAoGBAJVMMtZ4pF8D6mLMRZGR
pQjNPy+MH13XYmDRc/BSF8KJ4yKk3tohr9LSXzxR0SEB43NoL1bHkucZrNjGyL8x
jktnwkoIs96kO2mPrl1TqWkXs5RjGkkSTbAJovIcvkRU31SWap/WzN2kHpmRVcQc
KEFKXT5fUYZCLLWxhgZFlGPp
-----END PRIVATE KEY-----

View File

@@ -1,40 +0,0 @@
server {
listen 80;
listen [::]:80;
listen 443 ssl;
listen 8448 ssl;
listen [::]:443 ssl;
listen [::]:8448 ssl;
server_name synapse.localhost;
ssl_certificate /root/ssl/cert.pem;
ssl_certificate_key /root/ssl/key.pem;
# well-known config adding rtc_foci backend
# Note well-known is currently not effective due to:
# https://spec.matrix.org/v1.12/client-server-api/#well-known-uri the spec
# says it must be at https://$server_name/... (implied port 443) Hence, we
# currently rely for local development environment on deprecated config.json
# setting for livekit_service_url
location /.well-known/matrix/client {
return 200 '{"m.homeserver": {"base_url": "http://synapse.localhost:8008"}, "org.matrix.msc4143.rtc_foci": [{"type": "livekit", "livekit_service_url": "http://localhost:8080"}]}';
default_type application/json;
add_header Access-Control-Allow-Origin *;
}
# Reverse proxy for Matrix Synapse Homeserver
# This is also required for development environment.
# Reason: the lk-jwt-service uses the federation API for the openid token
# verification, which requires TLS
location / {
proxy_pass "http://homeserver:8008";
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
error_page 500 502 503 504 /50x.html;
}

View File

@@ -1,13 +1,10 @@
{
"default_server_config": {
"m.homeserver": {
"base_url": "http://synapse.localhost:8008",
"server_name": "synapse.localhost"
"base_url": "https://synapse.m.localhost",
"server_name": "synapse.m.localhost"
}
},
"livekit": {
"livekit_service_url": "http://localhost:8009"
},
"features": {
"feature_use_device_session_member_events": true
},

View File

@@ -7,7 +7,7 @@ services:
hostname: auth-server
environment:
- LK_JWT_PORT=8080
- LIVEKIT_URL=ws://localhost:7880
- LIVEKIT_URL=wss://matrix-rtc.m.localhost/livekit/sfu
- LIVEKIT_KEY=devkey
- LIVEKIT_SECRET=secret
# If the configured homeserver runs on localhost, it'll probably be using
@@ -18,12 +18,13 @@ services:
condition: on-failure
ports:
# HOST_PORT:CONTAINER_PORT
- 8009:8080
- 8080:8080
networks:
- ecbackend
livekit:
image: livekit/livekit-server:latest
hostname: livekit-sfu
command: --dev --config /etc/livekit.yaml
restart: unless-stopped
# The SFU seems to work far more reliably when we let it share the host
@@ -81,17 +82,22 @@ services:
- ecbackend
nginx:
# openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout tls_localhost_key.pem -out tls_localhost_cert.pem -subj "/C=GB/ST=London/L=London/O=Alros/OU=IT Department/CN=localhost"
hostname: synapse.localhost
# see backend/dev_tls_setup for how to generate the tls certs
hostname: synapse.m.localhost
image: nginx:latest
volumes:
- ./backend/tls_localhost_nginx.conf:/etc/nginx/conf.d/default.conf:Z
- ./backend/tls_localhost_key.pem:/root/ssl/key.pem:Z
- ./backend/tls_localhost_cert.pem:/root/ssl/cert.pem:Z
- ./backend/dev_nginx.conf:/etc/nginx/conf.d/default.conf:Z
- ./backend/dev_tls_m.localhost.key:/root/ssl/key.pem:Z
- ./backend/dev_tls_m.localhost.crt:/root/ssl/cert.pem:Z
ports:
# HOST_PORT:CONTAINER_PORT
- "80:80"
- "443:443"
- "8008:80"
- "4443:443"
- "8448:8448"
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
- synapse
networks:

View File

@@ -27,6 +27,10 @@ export default {
// then Knip will flag it as a false positive
// https://github.com/webpro-nl/knip/issues/766
"@vector-im/compound-web",
// We need this so that TypeScript is happy with @livekit/track-processors.
// This might be a bug in the LiveKit repo but for now we fix it on the
// Element Call side.
"@types/dom-mediacapture-transform",
"matrix-widget-api",
],
ignoreExportsUsedInFile: true,

View File

@@ -62,6 +62,7 @@
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.1",
"@types/content-type": "^1.1.5",
"@types/dom-mediacapture-transform": "^0.1.11",
"@types/grecaptcha": "^3.0.9",
"@types/jsdom": "^21.1.7",
"@types/lodash-es": "^4.17.12",
@@ -77,7 +78,6 @@
"@use-gesture/react": "^10.2.11",
"@vector-im/compound-design-tokens": "^3.0.0",
"@vector-im/compound-web": "^7.2.0",
"@vitejs/plugin-basic-ssl": "^1.0.1",
"@vitejs/plugin-react": "^4.0.1",
"@vitest/coverage-v8": "^3.0.0",
"babel-plugin-transform-vite-meta-env": "^1.0.3",

View File

@@ -0,0 +1,4 @@
services:
synapse:
volumes:
- ./backend/playwright_homeserver.yaml:/data/cfg/homeserver.yaml:Z

View File

@@ -1,97 +1,2 @@
networks:
ecbackend:
services:
auth-service:
image: ghcr.io/element-hq/lk-jwt-service:latest-ci
hostname: auth-server
environment:
- LK_JWT_PORT=8080
- LIVEKIT_URL=ws://localhost:7880
- LIVEKIT_KEY=devkey
- LIVEKIT_SECRET=secret
# If the configured homeserver runs on localhost, it'll probably be using
# a self-signed certificate
- LIVEKIT_INSECURE_SKIP_VERIFY_TLS=YES_I_KNOW_WHAT_I_AM_DOING
deploy:
restart_policy:
condition: on-failure
ports:
# HOST_PORT:CONTAINER_PORT
- 8009:8080
networks:
- ecbackend
livekit:
image: livekit/livekit-server:latest
command: --dev --config /etc/livekit.yaml
restart: unless-stopped
# The SFU seems to work far more reliably when we let it share the host
# network rather than opening specific ports (but why?? we're not missing
# any…)
ports:
# HOST_PORT:CONTAINER_PORT
- 7880:7880/tcp
- 7881:7881/tcp
- 7882:7882/tcp
- 50100-50200:50100-50200/udp
volumes:
- ./backend/dev_livekit.yaml:/etc/livekit.yaml:Z
networks:
- ecbackend
redis:
image: redis:6-alpine
command: redis-server /etc/redis.conf
ports:
# HOST_PORT:CONTAINER_PORT
- 6379:6379
volumes:
- ./backend/redis.conf:/etc/redis.conf:Z
networks:
- ecbackend
element-web:
image: ghcr.io/element-hq/element-web:develop
volumes:
- ./backend/ew.test.config.json:/app/config.json
environment:
ELEMENT_WEB_PORT: 81
ports:
- "8081:81"
networks:
- ecbackend
synapse:
hostname: homeserver
image: docker.io/matrixdotorg/synapse:latest
environment:
- SYNAPSE_CONFIG_PATH=/data/cfg/homeserver.yaml
# Needed for rootless podman-compose such that the uid/gid mapping does
# fit local user uid. If the container runs as root (uid 0) it is fine as
# it actually maps to your non-root user on the host (e.g. 1000).
# Otherwise uid mapping will not match your non-root user.
- UID=0
- GID=0
volumes:
- ./backend/synapse_tmp:/data:Z
- ./backend/playwright_homeserver.yaml:/data/cfg/homeserver.yaml:Z
networks:
- ecbackend
nginx:
# openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout tls_localhost_key.pem -out tls_localhost_cert.pem -subj "/C=GB/ST=London/L=London/O=Alros/OU=IT Department/CN=localhost"
hostname: synapse.localhost
image: nginx:latest
volumes:
- ./backend/tls_localhost_nginx.conf:/etc/nginx/conf.d/default.conf:Z
- ./backend/tls_localhost_key.pem:/root/ssl/key.pem:Z
- ./backend/tls_localhost_cert.pem:/root/ssl/cert.pem:Z
ports:
# HOST_PORT:CONTAINER_PORT
- "8008:80"
- "4443:443"
depends_on:
- synapse
networks:
- ecbackend
include:
- dev-backend-docker-compose.yml

View File

@@ -1,146 +0,0 @@
/* eslint-disable */
// The contents of this file below the line are copied from
// @types/dom-mediacapture-transform, which is inlined here into Element Call so
// that we can apply the patch to @types/dom-webcodecs found in
// ./dom-webcodecs.d.ts, which it depends on.
// (https://github.com/DefinitelyTyped/DefinitelyTyped/pull/72625)
// Once that PR is merged and released, we can remove this file and return to
// depending on @types/dom-mediacapture-transform.
// -----------------------------------------------------------------------------
// This project is licensed under the MIT license.
// Copyrights are respective of each contributor listed at the beginning of each definition file.
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// In general, these types are only available behind a command line flag or an origin trial in
// Chrome 90+.
// This API depends on WebCodecs.
// Versioning:
// Until the above-mentioned spec is finalized, the major version number is 0. Although not
// necessary for version 0, consider incrementing the minor version number for breaking changes.
// The following modify existing DOM types to allow defining type-safe APIs on audio and video tracks.
/** Specialize MediaStreamTrack so that we can refer specifically to an audio track. */
interface MediaStreamAudioTrack extends MediaStreamTrack {
readonly kind: "audio";
clone(): MediaStreamAudioTrack;
}
/** Specialize MediaStreamTrack so that we can refer specifically to a video track. */
interface MediaStreamVideoTrack extends MediaStreamTrack {
readonly kind: "video";
clone(): MediaStreamVideoTrack;
}
/** Assert that getAudioTracks and getVideoTracks return the tracks with the appropriate kind. */
interface MediaStream {
getAudioTracks(): MediaStreamAudioTrack[];
getVideoTracks(): MediaStreamVideoTrack[];
}
// The following were originally generated from the spec using
// https://github.com/microsoft/TypeScript-DOM-lib-generator, then heavily modified.
/**
* A track sink that is capable of exposing the unencoded frames from the track to a
* ReadableStream, and exposes a control channel for signals going in the oppposite direction.
*/
interface MediaStreamTrackProcessor<T extends AudioData | VideoFrame> {
/**
* Allows reading the frames flowing through the MediaStreamTrack provided to the constructor.
*/
readonly readable: ReadableStream<T>;
/** Allows sending control signals to the MediaStreamTrack provided to the constructor. */
readonly writableControl: WritableStream<MediaStreamTrackSignal>;
}
declare var MediaStreamTrackProcessor: {
prototype: MediaStreamTrackProcessor<any>;
/** Constructor overrides based on the type of track. */
new (
init: MediaStreamTrackProcessorInit & { track: MediaStreamAudioTrack },
): MediaStreamTrackProcessor<AudioData>;
new (
init: MediaStreamTrackProcessorInit & { track: MediaStreamVideoTrack },
): MediaStreamTrackProcessor<VideoFrame>;
};
interface MediaStreamTrackProcessorInit {
track: MediaStreamTrack;
/**
* If media frames are not read from MediaStreamTrackProcessor.readable quickly enough, the
* MediaStreamTrackProcessor will internally buffer up to maxBufferSize of the frames produced
* by the track. If the internal buffer is full, each time the track produces a new frame, the
* oldest frame in the buffer will be dropped and the new frame will be added to the buffer.
*/
maxBufferSize?: number | undefined;
}
/**
* Takes video frames as input, and emits control signals that result from subsequent processing.
*/
interface MediaStreamTrackGenerator<T extends AudioData | VideoFrame>
extends MediaStreamTrack {
/**
* Allows writing media frames to the MediaStreamTrackGenerator, which is itself a
* MediaStreamTrack. When a frame is written to writable, the frames close() method is
* automatically invoked, so that its internal resources are no longer accessible from
* JavaScript.
*/
readonly writable: WritableStream<T>;
/**
* Allows reading control signals sent from any sinks connected to the
* MediaStreamTrackGenerator.
*/
readonly readableControl: ReadableStream<MediaStreamTrackSignal>;
}
type MediaStreamAudioTrackGenerator = MediaStreamTrackGenerator<AudioData> &
MediaStreamAudioTrack;
type MediaStreamVideoTrackGenerator = MediaStreamTrackGenerator<VideoFrame> &
MediaStreamVideoTrack;
declare var MediaStreamTrackGenerator: {
prototype: MediaStreamTrackGenerator<any>;
/** Constructor overrides based on the type of track. */
new (
init: MediaStreamTrackGeneratorInit & {
kind: "audio";
signalTarget?: MediaStreamAudioTrack | undefined;
},
): MediaStreamAudioTrackGenerator;
new (
init: MediaStreamTrackGeneratorInit & {
kind: "video";
signalTarget?: MediaStreamVideoTrack | undefined;
},
): MediaStreamVideoTrackGenerator;
};
interface MediaStreamTrackGeneratorInit {
kind: MediaStreamTrackGeneratorKind;
/**
* (Optional) track to which the MediaStreamTrackGenerator will automatically forward control
* signals. If signalTarget is provided and signalTarget.kind and kind do not match, the
* MediaStreamTrackGenerators constructor will raise an exception.
*/
signalTarget?: MediaStreamTrack | undefined;
}
type MediaStreamTrackGeneratorKind = "audio" | "video";
type MediaStreamTrackSignalType = "request-frame";
interface MediaStreamTrackSignal {
signalType: MediaStreamTrackSignalType;
}

View File

@@ -1,745 +0,0 @@
/* eslint-disable */
// The contents of this file below the line are copied from
// @types/dom-webcodecs, which is inlined here into Element Call so that we can
// apply the patch https://github.com/DefinitelyTyped/DefinitelyTyped/pull/72625
// which is needed for TypeScript 5.8 compatibility. Once that PR is merged and
// released, we can remove this file and return to depending on
// @types/dom-webcodecs.
// -----------------------------------------------------------------------------
// This project is licensed under the MIT license.
// Copyrights are respective of each contributor listed at the beginning of each definition file.
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// Versioning:
// Until the WebCodecs spec is finalized, the major version number is 0. I have chosen to use minor
// version 1 to denote the API as defined by the IDL files from the Chromium repo at
// https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/webcodecs.
// Please use a version number above 0.1 if using the spec at https://w3c.github.io/webcodecs/ as
// the source.
// The declarations in webcodecs.generated.d.ts have been generated using the code in
// https://github.com/yume-chan/webcodecs-lib-generator. See
// https://github.com/yume-chan/webcodecs-lib-generator/blob/main/README.md for more detail.
// The following declarations are copied from
// https://github.com/microsoft/TypeScript-DOM-lib-generator/blob/a75338e1ea8a958bf08a5745141d2ab8f14ba2ca/baselines/dom.generated.d.ts
// and modified to expand the types to include VideoFrame.
/** Shim for OffscreenCanvas, which was removed in TS 4.4 */
interface OffscreenCanvas extends EventTarget {}
/**
* Replaces CanvasImageSource; only applies if WebCodecs is available.
*/
type CanvasImageSourceWebCodecs =
| HTMLOrSVGImageElement
| HTMLVideoElement
| HTMLCanvasElement
| ImageBitmap
| OffscreenCanvas
| VideoFrame;
interface CanvasRenderingContext2D {
drawImage(image: CanvasImageSourceWebCodecs, dx: number, dy: number): void;
drawImage(
image: CanvasImageSourceWebCodecs,
dx: number,
dy: number,
dw: number,
dh: number,
): void;
drawImage(
image: CanvasImageSourceWebCodecs,
sx: number,
sy: number,
sw: number,
sh: number,
dx: number,
dy: number,
dw: number,
dh: number,
): void;
createPattern(
image: CanvasImageSourceWebCodecs,
repetition: string | null,
): CanvasPattern | null;
}
interface OffscreenCanvasRenderingContext2D {
drawImage(image: CanvasImageSourceWebCodecs, dx: number, dy: number): void;
drawImage(
image: CanvasImageSourceWebCodecs,
dx: number,
dy: number,
dw: number,
dh: number,
): void;
drawImage(
image: CanvasImageSourceWebCodecs,
sx: number,
sy: number,
sw: number,
sh: number,
dx: number,
dy: number,
dw: number,
dh: number,
): void;
createPattern(
image: CanvasImageSourceWebCodecs,
repetition: string | null,
): CanvasPattern | null;
}
/**
* Replaces ImageBitmapSource; only applies if WebCodecs is available.
*/
type ImageBitmapSourceWebCodecs = CanvasImageSourceWebCodecs | Blob | ImageData;
declare function createImageBitmap(
image: ImageBitmapSourceWebCodecs,
options?: ImageBitmapOptions,
): Promise<ImageBitmap>;
declare function createImageBitmap(
image: ImageBitmapSourceWebCodecs,
sx: number,
sy: number,
sw: number,
sh: number,
options?: ImageBitmapOptions,
): Promise<ImageBitmap>;
/**
* Replaces TexImageSource; only applies if WebCodecs is available.
*/
type TexImageSourceWebCodecs =
| ImageBitmap
| ImageData
| HTMLImageElement
| HTMLCanvasElement
| HTMLVideoElement
| OffscreenCanvas
| VideoFrame;
interface WebGLRenderingContextOverloads {
texImage2D(
target: GLenum,
level: GLint,
internalformat: GLint,
format: GLenum,
type: GLenum,
source: TexImageSourceWebCodecs,
): void;
texSubImage2D(
target: GLenum,
level: GLint,
xoffset: GLint,
yoffset: GLint,
format: GLenum,
type: GLenum,
source: TexImageSourceWebCodecs,
): void;
}
interface WebGL2RenderingContextBase {
texImage3D(
target: GLenum,
level: GLint,
internalformat: GLint,
width: GLsizei,
height: GLsizei,
depth: GLsizei,
border: GLint,
format: GLenum,
type: GLenum,
source: TexImageSourceWebCodecs,
): void;
texSubImage3D(
target: GLenum,
level: GLint,
xoffset: GLint,
yoffset: GLint,
zoffset: GLint,
width: GLsizei,
height: GLsizei,
depth: GLsizei,
format: GLenum,
type: GLenum,
source: TexImageSourceWebCodecs,
): void;
}
interface WebGL2RenderingContextOverloads {
texImage2D(
target: GLenum,
level: GLint,
internalformat: GLint,
format: GLenum,
type: GLenum,
source: TexImageSourceWebCodecs,
): void;
texImage2D(
target: GLenum,
level: GLint,
internalformat: GLint,
width: GLsizei,
height: GLsizei,
border: GLint,
format: GLenum,
type: GLenum,
source: TexImageSourceWebCodecs,
): void;
texSubImage2D(
target: GLenum,
level: GLint,
xoffset: GLint,
yoffset: GLint,
format: GLenum,
type: GLenum,
source: TexImageSourceWebCodecs,
): void;
texSubImage2D(
target: GLenum,
level: GLint,
xoffset: GLint,
yoffset: GLint,
width: GLsizei,
height: GLsizei,
format: GLenum,
type: GLenum,
source: TexImageSourceWebCodecs,
): void;
}
/////////////////////////////
/// webcodecs APIs
/////////////////////////////
interface AudioDataCopyToOptions {
format?: AudioSampleFormat | undefined;
frameCount?: number | undefined;
frameOffset?: number | undefined;
planeIndex: number;
}
interface AudioDataInit {
data: AllowSharedBufferSource;
format: AudioSampleFormat;
numberOfChannels: number;
numberOfFrames: number;
sampleRate: number;
timestamp: number;
}
interface AudioDecoderConfig {
codec: string;
description?: AllowSharedBufferSource | undefined;
numberOfChannels: number;
sampleRate: number;
}
interface AudioDecoderInit {
error: WebCodecsErrorCallback;
output: AudioDataOutputCallback;
}
interface AudioDecoderSupport {
config?: AudioDecoderConfig;
supported?: boolean;
}
interface AudioEncoderConfig {
bitrate?: number | undefined;
codec: string;
numberOfChannels: number;
sampleRate: number;
}
interface AudioEncoderInit {
error: WebCodecsErrorCallback;
output: EncodedAudioChunkOutputCallback;
}
interface AudioEncoderSupport {
config?: AudioEncoderConfig;
supported?: boolean;
}
interface AvcEncoderConfig {
format?: AvcBitstreamFormat | undefined;
}
interface EncodedAudioChunkInit {
data: AllowSharedBufferSource;
duration?: number | undefined;
timestamp: number;
type: EncodedAudioChunkType;
}
interface EncodedAudioChunkMetadata {
decoderConfig?: AudioDecoderConfig | undefined;
}
interface EncodedVideoChunkInit {
data: AllowSharedBufferSource;
duration?: number | undefined;
timestamp: number;
type: EncodedVideoChunkType;
}
interface EncodedVideoChunkMetadata {
decoderConfig?: VideoDecoderConfig | undefined;
temporalLayerId?: number | undefined;
}
interface ImageDecodeOptions {
completeFramesOnly?: boolean | undefined;
frameIndex?: number | undefined;
}
interface ImageDecodeResult {
complete: boolean;
image: VideoFrame;
}
interface ImageDecoderInit {
colorSpaceConversion?: ColorSpaceConversion | undefined;
data: ImageBufferSource;
desiredHeight?: number | undefined;
desiredWidth?: number | undefined;
preferAnimation?: boolean | undefined;
premultiplyAlpha?: PremultiplyAlpha | undefined;
type: string;
}
interface PlaneLayout {
offset: number;
stride: number;
}
interface VideoColorSpaceInit {
fullRange?: boolean | null | undefined;
matrix?: VideoMatrixCoefficients | null | undefined;
primaries?: VideoColorPrimaries | null | undefined;
transfer?: VideoTransferCharacteristics | null | undefined;
}
interface VideoDecoderConfig {
codec: string;
codedHeight?: number | undefined;
codedWidth?: number | undefined;
colorSpace?: VideoColorSpaceInit | undefined;
description?: AllowSharedBufferSource | undefined;
displayAspectHeight?: number | undefined;
displayAspectWidth?: number | undefined;
hardwareAcceleration?: HardwarePreference | undefined;
optimizeForLatency?: boolean | undefined;
}
interface VideoDecoderInit {
error: WebCodecsErrorCallback;
output: VideoFrameOutputCallback;
}
interface VideoDecoderSupport {
config?: VideoDecoderConfig;
supported?: boolean;
}
interface VideoEncoderConfig {
alpha?: AlphaOption | undefined;
avc?: AvcEncoderConfig | undefined;
bitrate?: number | undefined;
bitrateMode?: VideoEncoderBitrateMode | undefined;
codec: string;
displayHeight?: number | undefined;
displayWidth?: number | undefined;
framerate?: number | undefined;
hardwareAcceleration?: HardwarePreference | undefined;
height: number;
latencyMode?: LatencyMode | undefined;
scalabilityMode?: string | undefined;
width: number;
}
interface VideoEncoderEncodeOptions {
keyFrame?: boolean;
}
interface VideoEncoderInit {
error: WebCodecsErrorCallback;
output: EncodedVideoChunkOutputCallback;
}
interface VideoEncoderSupport {
config?: VideoEncoderConfig;
supported?: boolean;
}
interface VideoFrameBufferInit {
codedHeight: number;
codedWidth: number;
colorSpace?: VideoColorSpaceInit | undefined;
displayHeight?: number | undefined;
displayWidth?: number | undefined;
duration?: number | undefined;
format: VideoPixelFormat;
layout?: PlaneLayout[] | undefined;
timestamp: number;
visibleRect?: DOMRectInit | undefined;
}
interface VideoFrameCopyToOptions {
layout?: PlaneLayout[] | undefined;
rect?: DOMRectInit | undefined;
}
interface VideoFrameInit {
alpha?: AlphaOption | undefined;
displayHeight?: number | undefined;
displayWidth?: number | undefined;
duration?: number | undefined;
timestamp?: number | undefined;
visibleRect?: DOMRectInit | undefined;
}
interface AudioData {
readonly duration: number;
readonly format: AudioSampleFormat | null;
readonly numberOfChannels: number;
readonly numberOfFrames: number;
readonly sampleRate: number;
readonly timestamp: number;
allocationSize(options: AudioDataCopyToOptions): number;
clone(): AudioData;
close(): void;
copyTo(
destination: AllowSharedBufferSource,
options: AudioDataCopyToOptions,
): void;
}
declare var AudioData: {
prototype: AudioData;
new (init: AudioDataInit): AudioData;
};
interface AudioDecoderEventMap {
dequeue: Event;
}
/** Available only in secure contexts. */
interface AudioDecoder {
readonly decodeQueueSize: number;
readonly state: CodecState;
ondequeue: ((this: AudioDecoder, ev: Event) => any) | null;
close(): void;
configure(config: AudioDecoderConfig): void;
decode(chunk: EncodedAudioChunk): void;
flush(): Promise<void>;
reset(): void;
addEventListener<K extends keyof AudioDecoderEventMap>(
type: K,
listener: (this: AudioDecoder, ev: AudioDecoderEventMap[K]) => any,
options?: boolean | AddEventListenerOptions,
): void;
addEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | AddEventListenerOptions,
): void;
removeEventListener<K extends keyof AudioDecoderEventMap>(
type: K,
listener: (this: AudioDecoder, ev: AudioDecoderEventMap[K]) => any,
options?: boolean | EventListenerOptions,
): void;
removeEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | EventListenerOptions,
): void;
}
declare var AudioDecoder: {
prototype: AudioDecoder;
new (init: AudioDecoderInit): AudioDecoder;
isConfigSupported(config: AudioDecoderConfig): Promise<AudioDecoderSupport>;
};
interface AudioEncoderEventMap {
dequeue: Event;
}
/** Available only in secure contexts. */
interface AudioEncoder {
readonly encodeQueueSize: number;
readonly state: CodecState;
ondequeue: ((this: AudioEncoder, ev: Event) => any) | null;
close(): void;
configure(config: AudioEncoderConfig): void;
encode(data: AudioData): void;
flush(): Promise<void>;
reset(): void;
addEventListener<K extends keyof AudioEncoderEventMap>(
type: K,
listener: (this: AudioEncoder, ev: AudioEncoderEventMap[K]) => any,
options?: boolean | AddEventListenerOptions,
): void;
addEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | AddEventListenerOptions,
): void;
removeEventListener<K extends keyof AudioEncoderEventMap>(
type: K,
listener: (this: AudioEncoder, ev: AudioEncoderEventMap[K]) => any,
options?: boolean | EventListenerOptions,
): void;
removeEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | EventListenerOptions,
): void;
}
declare var AudioEncoder: {
prototype: AudioEncoder;
new (init: AudioEncoderInit): AudioEncoder;
isConfigSupported(config: AudioEncoderConfig): Promise<AudioEncoderSupport>;
};
interface EncodedAudioChunk {
readonly byteLength: number;
readonly duration: number | null;
readonly timestamp: number;
readonly type: EncodedAudioChunkType;
copyTo(destination: AllowSharedBufferSource): void;
}
declare var EncodedAudioChunk: {
prototype: EncodedAudioChunk;
new (init: EncodedAudioChunkInit): EncodedAudioChunk;
};
interface EncodedVideoChunk {
readonly byteLength: number;
readonly duration: number | null;
readonly timestamp: number;
readonly type: EncodedVideoChunkType;
copyTo(destination: AllowSharedBufferSource): void;
}
declare var EncodedVideoChunk: {
prototype: EncodedVideoChunk;
new (init: EncodedVideoChunkInit): EncodedVideoChunk;
};
/** Available only in secure contexts. */
interface ImageDecoder {
readonly complete: boolean;
readonly completed: Promise<void>;
readonly tracks: ImageTrackList;
readonly type: string;
close(): void;
decode(options?: ImageDecodeOptions): Promise<ImageDecodeResult>;
reset(): void;
}
// declare var ImageDecoder: {
// prototype: ImageDecoder;
// new(init: ImageDecoderInit): ImageDecoder;
// isTypeSupported(type: string): Promise<boolean>;
// };
// interface ImageTrack {
// readonly animated: boolean;
// readonly frameCount: number;
// readonly repetitionCount: number;
// selected: boolean;
// }
// declare var ImageTrack: {
// prototype: ImageTrack;
// new(): ImageTrack;
// };
// interface ImageTrackList {
// readonly length: number;
// readonly ready: Promise<void>;
// readonly selectedIndex: number;
// readonly selectedTrack: ImageTrack | null;
// [index: number]: ImageTrack;
// }
// declare var ImageTrackList: {
// prototype: ImageTrackList;
// new(): ImageTrackList;
// };
interface VideoColorSpace {
readonly fullRange: boolean | null;
readonly matrix: VideoMatrixCoefficients | null;
readonly primaries: VideoColorPrimaries | null;
readonly transfer: VideoTransferCharacteristics | null;
toJSON(): VideoColorSpaceInit;
}
declare var VideoColorSpace: {
prototype: VideoColorSpace;
new (init?: VideoColorSpaceInit): VideoColorSpace;
};
interface VideoDecoderEventMap {
dequeue: Event;
}
/** Available only in secure contexts. */
interface VideoDecoder {
readonly decodeQueueSize: number;
readonly state: CodecState;
ondequeue: ((this: VideoDecoder, ev: Event) => any) | null;
close(): void;
configure(config: VideoDecoderConfig): void;
decode(chunk: EncodedVideoChunk): void;
flush(): Promise<void>;
reset(): void;
addEventListener<K extends keyof VideoDecoderEventMap>(
type: K,
listener: (this: VideoDecoder, ev: VideoDecoderEventMap[K]) => any,
options?: boolean | AddEventListenerOptions,
): void;
addEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | AddEventListenerOptions,
): void;
removeEventListener<K extends keyof VideoDecoderEventMap>(
type: K,
listener: (this: VideoDecoder, ev: VideoDecoderEventMap[K]) => any,
options?: boolean | EventListenerOptions,
): void;
removeEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | EventListenerOptions,
): void;
}
declare var VideoDecoder: {
prototype: VideoDecoder;
new (init: VideoDecoderInit): VideoDecoder;
isConfigSupported(config: VideoDecoderConfig): Promise<VideoDecoderSupport>;
};
interface VideoEncoderEventMap {
dequeue: Event;
}
/** Available only in secure contexts. */
interface VideoEncoder {
readonly encodeQueueSize: number;
readonly state: CodecState;
close(): void;
ondequeue: ((this: VideoEncoder, ev: Event) => any) | null;
configure(config: VideoEncoderConfig): void;
encode(frame: VideoFrame, options?: VideoEncoderEncodeOptions): void;
flush(): Promise<void>;
reset(): void;
addEventListener<K extends keyof VideoEncoderEventMap>(
type: K,
listener: (this: VideoEncoder, ev: VideoEncoderEventMap[K]) => any,
options?: boolean | AddEventListenerOptions,
): void;
addEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | AddEventListenerOptions,
): void;
removeEventListener<K extends keyof VideoEncoderEventMap>(
type: K,
listener: (this: VideoEncoder, ev: VideoEncoderEventMap[K]) => any,
options?: boolean | EventListenerOptions,
): void;
removeEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | EventListenerOptions,
): void;
}
declare var VideoEncoder: {
prototype: VideoEncoder;
new (init: VideoEncoderInit): VideoEncoder;
isConfigSupported(config: VideoEncoderConfig): Promise<VideoEncoderSupport>;
};
interface VideoFrame {
readonly codedHeight: number;
readonly codedRect: DOMRectReadOnly | null;
readonly codedWidth: number;
readonly colorSpace: VideoColorSpace;
readonly displayHeight: number;
readonly displayWidth: number;
readonly duration: number | null;
readonly format: VideoPixelFormat | null;
readonly timestamp: number;
readonly visibleRect: DOMRectReadOnly | null;
allocationSize(options?: VideoFrameCopyToOptions): number;
clone(): VideoFrame;
close(): void;
copyTo(
destination: AllowSharedBufferSource,
options?: VideoFrameCopyToOptions,
): Promise<PlaneLayout[]>;
}
declare var VideoFrame: {
prototype: VideoFrame;
new (source: CanvasImageSource, init?: VideoFrameInit): VideoFrame;
new (data: AllowSharedBufferSource, init: VideoFrameBufferInit): VideoFrame;
};
interface AudioDataOutputCallback {
(output: AudioData): void;
}
interface EncodedAudioChunkOutputCallback {
(output: EncodedAudioChunk, metadata: EncodedAudioChunkMetadata): void;
}
interface EncodedVideoChunkOutputCallback {
(chunk: EncodedVideoChunk, metadata: EncodedVideoChunkMetadata): void;
}
interface VideoFrameOutputCallback {
(output: VideoFrame): void;
}
interface WebCodecsErrorCallback {
(error: DOMException): void;
}
// type AllowSharedBufferSource = ArrayBuffer | ArrayBufferView;
// type BitrateMode = "constant" | "variable";
// type ImageBufferSource = ArrayBuffer | ArrayBufferView | ReadableStream;
// type AlphaOption = "discard" | "keep";
// type AudioSampleFormat = "f32" | "f32-planar" | "s16" | "s16-planar" | "s32" | "s32-planar" | "u8" | "u8-planar";
// type AvcBitstreamFormat = "annexb" | "avc";
// type CodecState = "closed" | "configured" | "unconfigured";
// type EncodedAudioChunkType = "delta" | "key";
// type EncodedVideoChunkType = "delta" | "key";
type HardwarePreference =
| "no-preference"
| "prefer-hardware"
| "prefer-software";
// type LatencyMode = "quality" | "realtime";
// type VideoColorPrimaries = "bt470bg" | "bt709" | "smpte170m";
// type VideoMatrixCoefficients = "bt470bg" | "bt709" | "rgb" | "smpte170m";
// type VideoPixelFormat = "BGRA" | "BGRX" | "I420" | "I420A" | "I422" | "I444" | "NV12" | "RGBA" | "RGBX";
// type VideoTransferCharacteristics = "bt709" | "iec61966-2-1" | "smpte170m";

View File

@@ -136,6 +136,11 @@ export class Initializer {
lookup: () => getUrlParams().lang ?? undefined,
});
// Synchronise the HTML lang attribute with the i18next language
i18n.on("languageChanged", (lng) => {
document.documentElement.lang = lng;
});
await i18n
.use(Backend)
.use(languageDetector)

View File

@@ -11,8 +11,8 @@ import { createHtmlPlugin } from "vite-plugin-html";
import { codecovVitePlugin } from "@codecov/vite-plugin";
import { sentryVitePlugin } from "@sentry/vite-plugin";
import react from "@vitejs/plugin-react";
import basicSsl from "@vitejs/plugin-basic-ssl";
import { realpathSync } from "fs";
import * as fs from "node:fs";
// https://vitejs.dev/config/
export default defineConfig(({ mode, packageType }) => {
@@ -24,7 +24,6 @@ export default defineConfig(({ mode, packageType }) => {
process.env.VITE_PACKAGE = packageType ?? "full";
const plugins = [
react(),
basicSsl(),
svgrPlugin({
svgrOptions: {
// This enables ref forwarding on SVGR components, which is needed, for
@@ -84,6 +83,10 @@ export default defineConfig(({ mode, packageType }) => {
server: {
port: 3000,
fs: { allow },
https: {
key: fs.readFileSync("./backend/dev_tls_m.localhost.key"),
cert: fs.readFileSync("./backend/dev_tls_m.localhost.crt"),
},
},
build: {
sourcemap: true,

View File

@@ -4621,6 +4621,22 @@ __metadata:
languageName: node
linkType: hard
"@types/dom-mediacapture-transform@npm:^0.1.11":
version: 0.1.11
resolution: "@types/dom-mediacapture-transform@npm:0.1.11"
dependencies:
"@types/dom-webcodecs": "npm:*"
checksum: 10c0/19c76d54cf31aa2a925011fc5f973dff9a10bdecfdf2285e5e568e61850a0fa2b8c9f1807a1462cbefd57ec26d32eeaa9c359117aca9d9fe7f0d6f2fff33f51e
languageName: node
linkType: hard
"@types/dom-webcodecs@npm:*":
version: 0.1.15
resolution: "@types/dom-webcodecs@npm:0.1.15"
checksum: 10c0/1407f0352156c99c9b4378fb4c0c799b061520d031903a7f359ad09a6f706cc1fd56bafb272bb1a3decffcb32e54a51d2f07442eb72622464a950cff7f9e8862
languageName: node
linkType: hard
"@types/estree@npm:1.0.6, @types/estree@npm:^1.0.0":
version: 1.0.6
resolution: "@types/estree@npm:1.0.6"
@@ -5127,15 +5143,6 @@ __metadata:
languageName: node
linkType: hard
"@vitejs/plugin-basic-ssl@npm:^1.0.1":
version: 1.2.0
resolution: "@vitejs/plugin-basic-ssl@npm:1.2.0"
peerDependencies:
vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0
checksum: 10c0/0d360fcca01f91ade6e451edbea09a107ff9e95cd3c3766c7a069d1a168709df92d96c0bd1eccc66e2739a153e07c75a45321ec487450c0da942606200d8441d
languageName: node
linkType: hard
"@vitejs/plugin-react@npm:^4.0.1":
version: 4.3.4
resolution: "@vitejs/plugin-react@npm:4.3.4"
@@ -6953,6 +6960,7 @@ __metadata:
"@testing-library/react": "npm:^16.0.0"
"@testing-library/user-event": "npm:^14.5.1"
"@types/content-type": "npm:^1.1.5"
"@types/dom-mediacapture-transform": "npm:^0.1.11"
"@types/grecaptcha": "npm:^3.0.9"
"@types/jsdom": "npm:^21.1.7"
"@types/lodash-es": "npm:^4.17.12"
@@ -6968,7 +6976,6 @@ __metadata:
"@use-gesture/react": "npm:^10.2.11"
"@vector-im/compound-design-tokens": "npm:^3.0.0"
"@vector-im/compound-web": "npm:^7.2.0"
"@vitejs/plugin-basic-ssl": "npm:^1.0.1"
"@vitejs/plugin-react": "npm:^4.0.1"
"@vitest/coverage-v8": "npm:^3.0.0"
babel-plugin-transform-vite-meta-env: "npm:^1.0.3"