From 5f54bde55e7cfefb625f2dd578e31f38234963d2 Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Sun, 8 Mar 2026 10:22:55 +0100 Subject: [PATCH 01/50] chore: project initialization with infrastructure definition and structure - Generate SSH deployment key (Ed25519) for server access - Define complete server infrastructure (ProxmoxVE VM, Docker, networking) - Create ACCESS.md with all connection details and SSH instructions - Create INFRASTRUCTURE.md with VM setup guide and service architecture - Set up project directory structure per briefing specification - Add .env.example with all required environment variables - Add .gitignore for Node.js/Docker/TypeScript project - Create comprehensive README.md for developer onboarding - Add Summarize.md changelog - Include concept and briefing documents Co-Authored-By: Claude Opus 4.6 --- .env.example | 77 ++++++ .forgejo/workflows/.gitkeep | 0 .gitignore | 63 +++++ .keys/deploy_ed25519 | 7 + .keys/deploy_ed25519.pub | 1 + CLAUDE_BRIEFING.docx | Bin 0 -> 32836 bytes INSIGHT_Konzept_v1.0.docx | Bin 0 -> 141557 bytes README.md | 206 +++++++++++++- Summarize.md | 66 +++++ config/prometheus/.gitkeep | 0 config/step-ca/.gitkeep | 0 config/traefik/.gitkeep | 0 docs/ACCESS.md | 202 ++++++++++++++ docs/INFRASTRUCTURE.md | 257 ++++++++++++++++++ packages/core-service/prisma/.gitkeep | 0 .../src/common/decorators/.gitkeep | 0 .../core-service/src/common/filters/.gitkeep | 0 .../core-service/src/common/guards/.gitkeep | 0 .../src/common/interceptors/.gitkeep | 0 packages/core-service/src/config/.gitkeep | 0 packages/core-service/src/core/auth/.gitkeep | 0 .../core-service/src/core/modules/.gitkeep | 0 .../core-service/src/core/tenants/.gitkeep | 0 packages/core-service/src/core/users/.gitkeep | 0 packages/core-service/src/prisma/.gitkeep | 0 packages/frontend/public/.gitkeep | 0 packages/frontend/src/admin/.gitkeep | 0 packages/frontend/src/auth/.gitkeep | 0 .../src/components/HelpPanel/.gitkeep | 0 .../src/components/HelpTooltip/.gitkeep | 0 packages/frontend/src/shell/.gitkeep | 0 templates/cv/default/.gitkeep | 0 32 files changed, 878 insertions(+), 1 deletion(-) create mode 100644 .env.example create mode 100644 .forgejo/workflows/.gitkeep create mode 100644 .gitignore create mode 100644 .keys/deploy_ed25519 create mode 100644 .keys/deploy_ed25519.pub create mode 100644 CLAUDE_BRIEFING.docx create mode 100644 INSIGHT_Konzept_v1.0.docx create mode 100644 Summarize.md create mode 100644 config/prometheus/.gitkeep create mode 100644 config/step-ca/.gitkeep create mode 100644 config/traefik/.gitkeep create mode 100644 docs/ACCESS.md create mode 100644 docs/INFRASTRUCTURE.md create mode 100644 packages/core-service/prisma/.gitkeep create mode 100644 packages/core-service/src/common/decorators/.gitkeep create mode 100644 packages/core-service/src/common/filters/.gitkeep create mode 100644 packages/core-service/src/common/guards/.gitkeep create mode 100644 packages/core-service/src/common/interceptors/.gitkeep create mode 100644 packages/core-service/src/config/.gitkeep create mode 100644 packages/core-service/src/core/auth/.gitkeep create mode 100644 packages/core-service/src/core/modules/.gitkeep create mode 100644 packages/core-service/src/core/tenants/.gitkeep create mode 100644 packages/core-service/src/core/users/.gitkeep create mode 100644 packages/core-service/src/prisma/.gitkeep create mode 100644 packages/frontend/public/.gitkeep create mode 100644 packages/frontend/src/admin/.gitkeep create mode 100644 packages/frontend/src/auth/.gitkeep create mode 100644 packages/frontend/src/components/HelpPanel/.gitkeep create mode 100644 packages/frontend/src/components/HelpTooltip/.gitkeep create mode 100644 packages/frontend/src/shell/.gitkeep create mode 100644 templates/cv/default/.gitkeep diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4b5c5be --- /dev/null +++ b/.env.example @@ -0,0 +1,77 @@ +# ============================================================ +# INSIGHT MVP - Umgebungsvariablen +# ============================================================ +# Kopiere diese Datei nach .env und befuelle die Werte. +# .env wird NIEMALS in Git committed! +# ============================================================ + +# --- Allgemein --- +NODE_ENV=development +APP_PORT=3000 +APP_URL=https://insight-dev.xinion.lan +FRONTEND_URL=https://insight-dev.xinion.lan +LOG_LEVEL=info + +# --- PostgreSQL --- +DB_HOST=pgbouncer +DB_PORT=6432 +DB_USER=insight +DB_PASSWORD= # Sicheres Passwort setzen! +DB_NAME=platform_core +DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME} + +# Direktverbindung (fuer Prisma Migrate, umgeht PgBouncer) +DB_DIRECT_HOST=postgres +DB_DIRECT_PORT=5432 +DATABASE_URL_DIRECT=postgresql://${DB_USER}:${DB_PASSWORD}@${DB_DIRECT_HOST}:${DB_DIRECT_PORT}/${DB_NAME} + +# --- Redis --- +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_PASSWORD= # Optional, aber empfohlen + +# --- JWT (RS256) --- +JWT_PRIVATE_KEY_PATH=/app/keys/jwt-private.pem +JWT_PUBLIC_KEY_PATH=/app/keys/jwt-public.pem +JWT_ACCESS_TOKEN_EXPIRY=15m +JWT_REFRESH_TOKEN_EXPIRY=7d +JWT_ISSUER=insight-platform + +# --- Bcrypt --- +BCRYPT_COST=12 + +# --- CORS --- +CORS_ORIGINS=https://insight-dev.xinion.lan + +# --- Rate Limiting --- +THROTTLE_TTL=60000 +THROTTLE_LIMIT=200 + +# --- Traefik --- +TRAEFIK_DASHBOARD_USER=admin +TRAEFIK_DASHBOARD_PASSWORD= # htpasswd Hash + +# --- step-ca (mTLS) --- +STEP_CA_URL=https://step-ca:9000 +STEP_CA_FINGERPRINT= # step-ca Root CA Fingerprint + +# --- SMTP (fuer Einladungs-E-Mails) --- +SMTP_HOST= +SMTP_PORT=587 +SMTP_USER= +SMTP_PASSWORD= +SMTP_FROM=noreply@xinion.de + +# --- Observability --- +GRAFANA_ADMIN_USER=admin +GRAFANA_ADMIN_PASSWORD= # Sicheres Passwort setzen! + +# --- MS SSO (Beta - noch nicht aktiv) --- +# MS_SSO_CLIENT_ENCRYPTION_KEY= # AES-256 Key fuer Client Secret Verschluesselung + +# --- KI-Hilfe-Chat (optional) --- +# ANTHROPIC_API_KEY= # Claude API Key +# AI_CHAT_ENABLED=false + +# --- DeepL (optional, fuer Hilfesystem-Uebersetzungen) --- +# DEEPL_API_KEY= diff --git a/.forgejo/workflows/.gitkeep b/.forgejo/workflows/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1017fdd --- /dev/null +++ b/.gitignore @@ -0,0 +1,63 @@ +# Dependencies +node_modules/ +.pnp +.pnp.js + +# Build output +dist/ +build/ +*.tsbuildinfo + +# Environment (NIEMALS committen!) +.env +.env.local +.env.*.local + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Docker volumes (lokal) +docker-data/ +postgres-data/ +redis-data/ +media-uploads/ + +# Logs +logs/ +*.log +npm-debug.log* + +# Test coverage +coverage/ + +# Prisma +packages/core-service/prisma/*.db +packages/core-service/prisma/*.db-journal + +# Generated Prisma Client +packages/core-service/node_modules/.prisma/ + +# Temporary files +tmp/ +temp/ +*.tmp + +# Certificates (generierte Zertifikate, nicht die CA-Config) +config/step-ca/secrets/ +config/step-ca/db/ +*.pem +*.key +*.crt +!config/step-ca/*.example + +# Backup files +*.bak +*.backup diff --git a/.keys/deploy_ed25519 b/.keys/deploy_ed25519 new file mode 100644 index 0000000..2e0d3f9 --- /dev/null +++ b/.keys/deploy_ed25519 @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACDLk6asy8o6kyAzCeG8BBOKNiXhx94pi/jXoqXrgX4k6AAAAKBprr69aa6+ +vQAAAAtzc2gtZWQyNTUxOQAAACDLk6asy8o6kyAzCeG8BBOKNiXhx94pi/jXoqXrgX4k6A +AAAECki73xblIq6Dx917rd90A5YrQwWVvp4RBMkU+RHsxNncuTpqzLyjqTIDMJ4bwEE4o2 +JeHH3imL+NeipeuBfiToAAAAGWluc2lnaHQtZGVwbG95QHhpbmlvbi5sYW4BAgME +-----END OPENSSH PRIVATE KEY----- diff --git a/.keys/deploy_ed25519.pub b/.keys/deploy_ed25519.pub new file mode 100644 index 0000000..1356590 --- /dev/null +++ b/.keys/deploy_ed25519.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMuTpqzLyjqTIDMJ4bwEE4o2JeHH3imL+NeipeuBfiTo insight-deploy@xinion.lan diff --git a/CLAUDE_BRIEFING.docx b/CLAUDE_BRIEFING.docx new file mode 100644 index 0000000000000000000000000000000000000000..bb4b3a7c713c19ad27cd4e8cd99b5f5a68ade0e0 GIT binary patch literal 32836 zcmeFYLv$`o6s{TD_K9uVc1~>DHc#w)v2C5$w)2H2wrwZ(zqcpdt9#PpduvdWs==?iKtM=9VsTm`qCr7GYN0_u&_Q6pbj2JU+{_)^4As1y%w6>v zz3lCXiy*+L3qZjBoBw~>|HeDeoH}JQ#Ec^G688NE*!oYlle%4k7Co`xby4BZjodAjE%)+_^PRy<_|DTuLBVwZuS1tF=Uo*^;WYvFiD76S%X zEN+chVh10rFxtjdytX0|DQz!0=_39z?48JCg^G63uT0To0AHfnx2rhr*Z>kBgv0M- zjTkb3Lx;>s)FVupMj=^`|E`zpMGYR4cQHy!XFk}`m5sDbpQ_uRP*w~&gy(!_Zdk|l~b5JY@PvC%r*}T)iCVfaij9HS5n5$cSr6yTphJMRL#pspNAGP-GU8nMk)v}2teb@ zR!v&sd0K(gVfy}Mdh;Sx8gFd`O%$^jsFN+df;|VlZ1cMS$?RdPk?`lg?ECov2T}fi zNSG)IukGf)5yk%$3jd#k4PDIbT$vdEXa9es`@gYX|8GmL0Sx>nT?FBqKVN@Yw)mPPlcAaX6Qhk8J%7A%$Up^P2KJP^SS?NaF_mPE(g+p z;~$O<&&U}TTT}ZejB7E0)Gcl{bsBRU@l>DM@04J(#8@;KC#Taaq9*SM?;&r%6HE{W z4dAylL^ZWJHG}sq3|K$_(e>WzYTuTkiCbJ#PRWGQufzC9(7lp;Bx%<7vmqvI>>Rll1%RE#(YUL^QeF2lw3GOE+~#{5|rM)0b{95K7w(oN5uY zW$2rG63*{~Is9{9}R?J784&1sP52nt>oczn3thR&a zz@3bynk=&(Uq+vB!e>jGZl7Hn^VTgI{U48flZ;ybs+}8mRAaTvdU$ZTiii(6s9XOP+ zmRH%c+6Rn;M}T@W_8o7L*VTQVp_)hi$##o;!{tx^`Jw5vOoCWCvd0kT88I5ZVh8WW&=;A0ftrn)ge%9x}T1!kdN%erM@PO32tQhd832Si+R&HxBv2RtD7{OK6_d{t86apvoW}@* zqaJ|xLyD9+UXt~iZ&ZFR{R(IKVx~sri25Z626I{Zhwoi5jBp*uK3QZsPXZob0FeIx z-+*ImF3_EMM{Xqo-6hERc@zQE^xtB5_D+lqRm8$~mcN_!u?kF`>I%laJ5f2=A^*V` zc(P3sSBeXVgn}lcTZju%aQGL_y&I19}nn3o&)!Sm`89N*`WAmra$@DY6;c`mw=L=R&=h2SCC zm?gOqBhxI7LBZ_qsTi`Rp{`9K+hU(YZ9tF;9?Ks@avJ!3(WjD#!BrB7c~Em@0fD^7 z%L7vymS#JFcl4nMTWZBrhG9IG>^^AL&^P9mJ#j(0Rb?OJ)>Sje1_8ts{SiN9A6ydv ztG4-D|C2!Oc5?^$&VJ^9Tu@L29dALit$|VVLa*#}jzpU(8$+5q?GTZbhqH)!?`uT# z0a60Bm3n3=St8L;l&*)|Ty<@vj`EP^EJ#N7`oEw|yZDnnpp^i2C`W?f#9%ej0KAFYpwNzGk zaKmu&shEeyvfe>%ozGDZA8S2DK-iZYKH#wlpW@G3+tgay8^<){ovI|%;{nXJaqRg* zrq*miF5z!8tK3s2tN1$a@SD?fsF_SU5vMNMf3lO7TyK#zuG)HCKd9Q4o?1V$eLl4} z?S{77gVe|&s*kJH`T$DTH)E>0r2NFM+Nv$NIem?;^t_%D4<^1$fc@R~!<`YQve2l? zvzBHC_2^%&N3fZCLO)>Ow4!%yn6EO~IeWgv>pLnIyZsi*rNi;QvTNYl?1c_7U3{%w zqQ(-zp`M!vrlc23bF;Mm5KYaJwnL0;@PwuF*fjBpABI@~`Ee^yj7bR>N z(~DUsus&m$qb6jWc&H>Dn-GcMzx+by1+)l}>yrK!xF;ipzJOO(oe?Z|(}lZq2Q#o%N9&!n$b3?^wU0!tv)Bhc4B|{W zBx*3!uwT3>@)PMIeh-#wes&}Lpxyq!g|w&I+`nutu-S3TAQ7ql*yKX@G;`~`0DVTmkm+XK+Obau}?W6Kq2nkiHZ$+5iNy9O4G=xhJx9ec}p z>3FakExW+B_`XFY0kQAfr?)pWGdCb8JioC-C>%<6W2HfN83EKHaLqTO z^ixq75k$A@CdVW*lDfgMzXkuX>qM;xw_nk?wNF+!D{w%?37adN#b6AZ^&#w<5BfcckCD7wW}_t9&%y!IuvEC6tXOFfHc+k%TBR_e3F&Z zkE#>gp~Mwvyyt>_gS)LuY+Z9fkb&jRc{Trr^;d05*&5Ri1X*?~(8%UHi#bM8?V7L&}j~BxuqOiO8@*gw0H-*{-s;6$@BvTJCq@ z9N-fibU=Dj&`!70Ed&4ZCK1XQaOEGyRlJ6Ng20Rzugt@c&THseI+hdl$HQlutyCFH z-#oBp=^}v$ey$KU18al37l~%?;@fADa>3z^?#$&u3vPgk)qK`8?K|k_o_D`J%)T7b zgWc9K>ZTW=-|CSJ;eUR~*}u#AN;$vF`3b!j2;_AI)Waqo1$}3$++RVAXQ4tBl3odR zt6=qH-9sc!iUtWb$CFQfgo3y(u#$ne_3q>Ifc zyzfWcX_X(^PHDr6^#-ss4U-Kmu$}*Cn3n9+9mOxS~V^S)zaOiea^94Vt=Xd#Q$e;$f-y|LVZI54q=V zjaAoiH4)NSr1c&NMj`VAA}kmU-QOD%?GM2yvhVTps%@ zdgk3BIt`M)-ai4hdnW0LC1=*+XMP?JR`>(MN5c%J93}Frplt^5XBO&C*ciSL^Pm;AJtm`J&L0!@IwI&+D4xEeYfye*C51oeT$ zHlPN&%pvA2bo8xA?E$u#wkc;8j0_-Um-R^SvS^#Lb(hgNuPHi)x?AXtq}~MYN*-|Vjbx567sJsDy9jfO#z!E~ueZu5FfrMqxE#lgE*^fwFt z;tKm@)+(EwYgh^62H-qg!IINJgJfbd-!K@E^)rlhwkN261dj@AiF%Di6qEF;Z*N72+p2`+Y4ZnJN)9E4)5eaQI-Z?hK#W5UEG%jMAc`YP-jon$-E&80Pt6N%<_;sW7TsrUR zukK)%bn=V%!@X;C#zkqT%Jl3;0CovM;=X3SyaZkNicf7_g2zux*7l;%Mkh|el%DoV zuBE{sKS8m`Oii+QwVe!fB-Ct<^0ZmFA?0`hY(x4PQqQ+7A5}*)Ws$(%IW)Rv82U6i zEnI413thDZKTO|9`=O5e<$R70xXjeDNSE)alDiUY#oXuyX?l zHnv*~3n0W!$y%pF1ehQ!@EF5>Leosn9bZ?V0n=~L`>sh?Q8ceD(Q@khEFOJh*`o73$$vo*2;25dK-D%68X`10si9eZ^mWt=S_Iku)ed((GBDL}67q z)rg5Svz(}wBrzGqS}{pOZ4O@Fu}dNcG3s&Xk12!$8ta!&X4)l4NtXAO|A2xdS%plx z*;Z?}LQomKq@(PL)t5#x(XPCHH0*7BOdMhr~$wZ z1++w5ugP%TDx;NhV!!|+)>kDEVN*1Mxj5;T1MM@y6Kw*?vZq#O8-^PmEv^%^mlR8)n{?2x1nU^ymFsx%GH0wVEj?a7CWN2N?c&&X-7hu__I*lBVI9}O^y>Z}uLU*Nx*ET`Z21M+}to$xCA z-)fwZ{qm$CYfUhc>VwK#2_eH1BQn^8MTX8AC{Ap^YAhPqQ*tw3s9dut6+KfXz=C&h z`Bgxu&Xx?u*kg7@AlhJ0+}?8zGkdKk*HcM{%V{MbNM)y*vv;Oo^u~>>Sa0PY?OFwf0YJCdrK75m?tk8Hf ztBhX*3hdgMI3t4;NfXzasJEKz7L{L$$_qUGPT#;dVVwhdxbXqzxSfp?Fmq-v$fzgG zGm@__7(6x6OxP7NFwa;aSL=Dj9gmwYVOjEXE_PxCWjE^hJ7x&Q-ksIslLqvGxk}uH zuKc9lNl^eQl8uewlfs(t;{dNL+)$e*Su|{WKxA`n;#F;tzBbab6H}4C0fxG9^0F@r z{aT8?f0uIkwrWGeJ0?`*~2^z2|Y$yv6ryP0r?}G1>5@p;iw{6lMjpP>U63ob6J~M?DtGpYfaYgsds1Hfn)g;^e>h>UNU4cwvRxRffW|_Z z`4BNYsF4DQWX1-0{OKakLLp|#vY_D|`!U$>7FOXAug%KZY9|;7Cus1XvR_|?{+ag{ z&{Hqze**Veo#|he8sclE(LF8;N3TEHXx7~v@sj)^vN8+INirs>-_yKPxd3S>vsV0% zh{XbQ%kxnj^(P@}|N+1E5Ml{V_kG=zOX&;A!38%oz-6zZQYJ4#NpfnRZmae@?G5f#wH7>Rj<*_j!`86^{CzrcbXmzYa}==+Uuxj`VjUmud| ztg(3X-`Cn*olIrptfk$`TMhK_;(jm(Q|dM`AfsC?w@NHM?Aj!t;3e(f-1fQ3p#dm! zxn<*bZP_yt4{u*>UtSkaV*W1@cHuS|QUzC4ZHwVN7pOO%ey3ASokwObZvcz9kul5Q z8iLKh=I4%){5Rv|rj8qz;F_^=jfG2mj5;87icT}g-Lr>+qpwk`+(rWqp=(ezaq59K z$3MhAgz{o=fcy(JOB`iyIWRCPrmq#K4F$Zxw2fYtokY-2hRNNx3q`icQAnUZZ`DR6 z@R~t?m}soZhzI`5vKrtw+6FN^z|p$}e~3p{rkk$~?h>(8zZE&n5`l*n|*v|FsjAe^H080^ixMOnYSvc4d2UVR|_xxLl^Zo(tR^ zctYa5^xyX2S`+2hlp$KJQpgUPGuTS^$J1jm+>Q~KqFBQfI#o2sirF-XH|nG&+AQ1E zG%+R+xmNCA*SuL%?iMCOF|L?rC{>(8yV(nS76%X|S@hi?KfE3_AKaK})c>L+2jQL!BF{tl(uX}e3HY?AY_5-apcH<7s+ z1Qs<*kHH%Cu^OY}dJkKa$kss=I#`tz@oos1*-^bJ{Z^ZB4Quix{N~FOC)@nSQ`j{~ zIMsU|EHp72-LR1d7hA5Der}rhbs(1=s#3))UD0 z6TiWW{8Ky}_Kwjt-r0GEIc&L%tg4Uav}MKY7z(!uE`9-aJgkUxOu17+4EeGlHv&`U z2utmEzFyjUQo4E&!-s#OV@vK6n@B>DCa@~>4UK(xA75B<8O>Aa7Iybgt`sOEXiE7E z(G=g~Ca_|y0&A`k4a2s~(_>+Hkzz$jCmjMYZ=u{UMZ8qBs{jaX41l-*qWzSd&%dRvR;NFi7;SyZla{iv?`B{FQ9*7n#EDr<^6S zrzzwV`S(^?^n6vz{}wz4cqcmzdpmb}{ny63qTK~2_6&AAXPt$5M%M^k0657ZjJppQL#4ubG3WsLd7-aukFyowN z*$_=Snq+U@Ad)ffD!LISL}{{h8zShka&h{fIZi9H|_x4C|&4% z&0H~E<9T-Q(&nNULQ{D9ow0eg@i7a* zX+=;0nmgV*O{}7>HnWw&V$*Y=3-J7V4@f+d6f2C0PQbS(y1W<`gAAzSY3*Q@~0t6v~bO)A~8`59Z5vfX1`Qg5#G{(D&@CrvKoN0RVxG zvb9~K>Kx#}8x8{5wY!@*3<+r|cbV;atD5s%#NB>%K*NxRyf#2UuL{1LD9U2pi=LJQ zah>K8T1AM=rZ?fNUns-}iaL=P%NbEB;&%=QSM9jTc0bz z6a1}jdT4ct>ZYETe{!iZa#y8U+5u_?irt$OLKkV5$tqX1OP|0+#S zW7V}m*1U*&3$5$U&N_|s4q`V_ICEI8L!agNgk?_&3Su7NsIC^Nqa-t$7UU7Zc9sY<4~n^raDZD!;4uqR3pWvD zSe!v%o9elk4`M7f3oK)yW6ZFUFx@t9on%F(yXkw(FdUR_c~MibE3B5T5}Os%teqlM zQ%SpOORJ~*tD|ByY9DpWG*uhtY~SKa>m#`>qOiC$@q<)IdVT0<0hsU`wZ!Jxq0hgA za_r{q)|Bmm@h#4rX58U&TTA{C4tri{rG)GE$I#LbEalrn*T+e4HhIPPaY9&j??unC zlF;tuxH53$O_opZ8RRgtpgbaqJfY*Jp*sKb@<*>)*G#n)u?6+t2sLFENAIl}u;$L5 z(-uMs#b`&DWb~UD&gUFH*LfaOu_^V+40y$I&tYE(3ZINls+Ernf<-9 zDoC&-vQ_hsfVK~xj3d*Z=OgQgKceBK*;U7vy?#|PaKNMP=K9UVO>_+&|IqzM+Gv#v zY~f@X_QK_MOO-q^H#zu}->v*4(a!OVz4|#ox9@e!QX$ItO6|PDAJFQ6Z1P!aZ1=7m zlnMBhk}sBbpG*H<;TH5jaC7q0SV@2L5LZ@;_v~oYehwU_ zE_2+sgox8!M$t5Yk!?H%4f};#TT0#{tg3CDvKI29>Y^QG$w6X&#z_BOeR;8`Om#TB zpBFxo$T>d59hZ=5h(7B{sr)^QZSd|Nvls`$#MTu%=g$m^-NS4;w&+p3!rk>gOKXJ|e>>bBH*uL-7w1E9K72rWdU02QltIc5Ka z5bIDEao0@?$2=d2Kj8|_)`N^WF?2Oe_dnq|_BJ_|AjpI}25Ct}3Zg&7zN5?W3TKrO z5le{3Kwxo#=}weR0_e4~jySLEUg9j=2Tukj3yon1N3hC*`4t%zJFg6#CBI(Oht+v& z&U+6o0v7w-cVODqL-3{zKpDBA@K)%e@5$U$j#80@2_8&R_OAPK`%jXx(`0^GLZ1>K zO9z1HM*g2j0w@R$k4MCf4yG z3pA68{*bP+Ob?K*3!0#O(&oxX7RPQ)!eSW;iQcuDX1z zZ8d}F!t5p1+@j1i(~r4XqWj;Zc{K^YS`iUyBD+IlUQ6KC<`_dr)IQ9Mg%|1QHLDUT zG}gQ*cIE##JBQA8MLQ4>mm>@E)MG&tYnKb;t6X3xEnHqhpGQaF&W=4fBnze@6tA2V zWdz9W^Px;VMjJ6s;sqb^uKT`BEaSzv`sF^a6|(?2qGa6)XNI>We1i&;4N~-4y6n_XP_sr+vk5=r>Ut$=ZS;AGMXTn$x9Cqmay8=0khFeU zb{cI=TLd55I$<#le=wyu#QM(kUbkhA&-cH{*xSJ)R0SBm(Jm=vpv=KglCSHnE{pEKde{o& zC73UH`+Y_mNw$J%`o1JbFWcO_$*z#8)tX7vGD6US;3Cgk5+I&_ie;3sEAwbEC%oD9 z&_N5BkHIQ9jzMyMp!5hYfnmjBypnkE=#A@uaziKN8TYn0wbo_Mc2VIG-%t$eD@=uG zSEF!=)5*&UR86&wAk$AgJ#llpD|Vt7ycDZ04fJbu8Bysi zirp(IgUW+yx-kiInWjWayKi+T+`iQS8#Y>R@92G)^r3yDv~E9X+rd`tZ6y5$^~9Q# z%yQE|)}irNoxb>P9O6C8@{6c)fzwU$FjkqE?2m&7IF`EvFbM)|>RS&XJz;~0DhU?M zz%N9xR+(JRs^Y~6etnMIXdQRX6x;3XL^H9wXAjy^l6wYeeW z{<4zV9Bcgji`+d!dj3O%G7pF?XAAxQ#RtEm5nHf|7HJ!v!SNQND2NRV)T{3!3yk^l z$}h1}zu!u-DgLb-uX_1a+T!wQEFv88_2p zsk2bxiQf5(EM(8e?3P2}n`I*sTCdWM*|cukDiHy3A+sE(sb4)hO)CIZ7meLg)nC5LJ@psy#vPZQRjMa@c}h-#4|XsH zR$yF&RBrx(dEy7KngiwLHfh&a-^N-q90Fcdkr}Ns)C>I3F7uO@1EgUAq6WMmY9fuG zvLF~3)SaE>bBz#>YuaM8l)TFun{_p4%C7zrNEko`x!tfV+#P%aI6VG*!^J7T7ar7L42qdEUwc)a#+ z?pyS=shnq;)GsJYf4xvWMVngrRM0p|pZn{HAHI1sJ-S~VF>UwQz6BdQRGnD-fl+!2 zaQ9F_*omK=48NwsW7~_nxRgu1n>r|wX?aej&|1O#gjppY&QYB%6>j1fS&Opusmx%i zH`di@=DZ;SYF`sMKem@uFGfxQ9{Zg)<7V>y5f7U8nCfes_O+EwmEDA8QF3w0bhJ$2 zNv##+Bi#mB5V3VLdNb&yqBttvrKzjJn^iV^W9W8N*=XZnp64tZG#&9>Vp2dTS+%fG z_frkBm@68t8hrl78*SroJ-}n-jC7KAR5kq(`tP0D&(Ed)h06waS^#EBpgdno+8`gz z93@+~$7Tb1!cj?^k8Kq^8VM|!K{K({*ld~2j`5CDz8SKxY?3`q8bg_`$7u;Y8oBYg zUZ1NI2!X-w;qo>!Nyb&-)R1xB;pH+Bqc@|X6nk)W(&w7*+xrd^^4l#0n|I>eZupd@ zZ#?Psp(fkC4byjM^#&U2{<68cuDgGtYhL-zX%O)45MQ8MuRDsLH?AZTeOP zAwjzrpC-D2N|)ztSfX10Cz428DPEGX6~)5*E?eJ$CMJWxHbvOV0F z;%Im4@B?c#z`5z_Xy4&`9lx;}S1Q23sl?_!w;KQOM*E9__Fbdzf-0gC*i*cw-!;QGtxa*2O)5m`C zd0Oo!k{mg+-5ii%L|+VSZGxD`e6Rd{o=V0gyYHpJk;66Zy-E=~(7a{;JZpj0;UKy? zDo|;bq@%7DGF}-dr0+cUo@HIA0=xb;Rew?|W^?3&-{GOB>goGm1P;!rWua_32vquR zlc$i!5Iq!{)O5H-H21eovW@K|LWkk4gfze`M@ByDFJ*XQPXu9F6_oX!+*>h!%cV}*kDmTF&jd7g zu+bTrqE#%fQ;=!Av{t5Uax6>M8}7IaO`7jqypmxiIODo0u(Cc-D7- z4iXygW?=AmxwdG%HU96G#-1>{jbQ9B#lMVW9N33K;;n%sEFzZP6C?$++_c=YCe!;E z$wdXG?35!;ztI9_x+)ez;=6gotYJMPm8Q!EwF~9IKBuzov^6TA^Ke@QFQk?)sGDo2IM)*v&{!tZv(qiuo7;ULS@Qap0A>U z6j4Hb>|RPeO7N>_Zp+~XKBq8cTy@x57A}5q4bkI5*Wcel@Ts)BD+CE`=_SvWX^-I-%GNIRdA(@Mz0Wfy^W_Xz3klY`Dv!=f+C zj9%mNgI&4$3*~appG|U-f+rdJ=7305z-zk}N5mPYklXSiqPi`3w#6H&kwF<#AX&;E zRKfE;srb?g7bCG|{XeY*{tCdF_##h?4?T4x%)6hqxW-#aBj4zq#v1K3LddXw@#VP) zWGT>2=J{T&yGY`s*^7C*@kr|RM)8yBFc6Pbd&(X;s-4V!ChJD(^~OKgrurq;2j2kAcJPYoWKdQc8?P2uEF1OAER?0^QFr;G*mx}$iD`HMM_!r-oJJ>9tW{ATXJ2AI7jx{vM4u~qZud7aNl z2+b%tk2saLUI;$q7!1+PmWr0r1(vk}%zndd<`yDBfqj-uh4)S$fr;thCb7iU1HroVyle>2!dBB$^2tf&Zi^bbI;Y?+j)C8goi(1eMk zvASX?=nbBy254REp@!M9MJ;x=kZ4+{izHAz>x3{d3VBk~wjJtHUo@JG1<_o!TJh?T zy|8GTz%#_q4zGXN$hEbJ{L-qMd{j_CY{s4~o|=66c|_nTva zx%kZ|q21utHf9O#8=2`2V?-6;0-1-{ri#;QaAJG&TRXdM&-sD_oJYS zvYYE2wNJD6$0f%eU&W{{uF0H=LDYBj;U@oZlDX3|JbYb`@UU?~Pz~SQ06GqbD8?QE z_Y6Xdz9y;(E2f3?+Y5D7Z={*slS)G|ji&Q!pB7(HE3dYS*alRS?q4=s>q%DO35N!4 zd+JqQdDaM&l_N~rh}H=^h4+L?w2*sJ)VtZbJ%kNzS*_Rmx`e426~mIx==nRlrr5KH z;S+Jzem^D>b_N`R?i;1-2sv@ zmELuDGL#&mV=W_fsRTX80#TDZ?){>~jvEGJ72dQ*Xd8+oE^NW~oqk{^%DZ zpn>*6yQ}dfVSxbSYFUQiaFrVn_ECb*B{*R(HPoXI&P#L*vi^j?oNi#8VXcjW z(GrY9G6(Il5|H6Lyz314I4#DVQ9NyrsoI>q$&V=^2n?nZeODsB4jFYIo1Hj^ z6sZu1V%X6+c@#)_kIT2O_ErRDR@_InAoq~! zSaT+q$$(OWQ0y^NcJw{UJwcY(PXiq8ehs^gIMwx7@#{iDLXwocg=;Yun<>VT3PkzC zMLxCJHY~^8_lhhC>MYax^M`FFkRMfoQOqzUye6>)ISdyr7p*$RJCw!+bcJP)9wPtf zPZs9FW;y?i$2kjVR?r#29;T?_1E6yz_Zf$Pbi#^it zr7PcOouAoSbIow|=W$hiDw16q?YdISa4-DcyIN_LTx}5xoP!MIyHU5aBU_m%&g9RE z6$=4vjIpqm|)jrfkIVqLNYa#8+kWb$BM7z=)4xog81N0YBxhQhu`6pw9o#=8Xy5TEB}A@Q5ZfeZzAX6>)J}8yb5{|kJIYsu zzboNTG?TOeh7(`Zs>+B?rZe1hLl1-kE|4O+mdpDyUpVn?RB*Wo>308g=6j^#E zNU(KNLHiJxPFhwE9Wb}1II}tz!Q!LAG(R&M-u;W~m*iE^3wQ%4G0}YJy^Wx}fH8Iq zM@g|}UdH5!*T=}TL#<0Uje2x`ALq{(ISNS;03 z>34zQ`^`?_7w(Kp3V6&s#SC1p&s!K@L)tk`9u|8m>ty`G9X9EyOOBlHe|$>@xPzsH z6%vm*bAzTP!6xSs7KIGjg@(B%02IF#D6aL}UKs-9heK3z%>B^8@aul@ey4?-zQF3K z!iggjN8-f4CCqkMoeS%>kW=G z-*BGP(&?3Lh%9GMf!bQ9>^MxpQK`)~r3N-+S7N@4>}Q5&@;}s0(6hP6 ztihNH6ZXt5K;B~KP-y5~eKenWVpnSEV+yHxIsC@gKzg~Zd6fH2^)fbo@|#Co>Q;N6?Dq6t(6vFk+gdL2?C4_@X^6-2j=R=cadN7bJ;@2oWK z+DSTC9R_onw(!KWOh*PA@9b8arHeHWM?SH>j$MvSANP1A?etKu=~c41o~D(EBt^0Gr`54Q&MZ z37rJQ0{|4O!itiCF(a{!BGOs1y`5Op)JTdGQ&cJJW+SLkn0Y4Q^CVXk!}U5MHpmWa z!f*aX_QbIiT0+z*M4Ny5q!C_C{%(?i%`sVpi$N2<`NRQj_Lk!O=Ku%PYQLKkqV2Iq zR=1Gn7J?_#Q_?}(%nRT^4hXll32*Um1)hjbah;r$=Q77wikgP8#;E<8qyjNqd^iYT z2of!am2=|xH+(@CUAocxz4=g~%JOk>6uKL#ghieLQC(Eh5s1sICkqCr)C&9Nrm0?= z=k@0XY-?tI@oM*Def^{0L;Dnp%9txI>BBwL7ajiOtqz@2^QDQCchgM89u?^SMT&Br zhswN%QShjKLO;hdOa#&#^C=k`0hjm(9F-MYlyo>U2P6QuuVkn)(xl`%H`R>0_E;X4 zPXn$+ypz=8Cv6Zg@rw57%syyFDq7$TEj(HzKW0X}e-C@CeX*IaAGGCZZSyW@|{hR3*Tz~ z%K+!iV=<;MuynUrM)K&(4ReD2%{9X8DgV_&re2ErJy8LDnlcou7U_b#1gEFNTo3pS z?D_`IInD;wGkpdWdZaq!APe;ft2$lWK2?ri)lj;Lgnz1aKiI=X$DnJcduYVPAcPUt zLNC!)U6Xi72cuq6w^loHfKm6Xfj?#aG&vh4N+O(93P@!93`OB=yFQK}QfYM)%~{Sn zsF8`d6x>``n=AC@#Uu^aTP~-AIID=K==SaJyXm=j>oh@fQ~&#&{mXZjp`Z-s9?Ks* zOQ_V38?`HO`b`5>g$U{XRhFoq3?Nkh^XvcXw|0z=BPsiT|G4`gK|lyV{zrS?;9l3Z z#T(mp8rx{vBuyIIwrw=FZQE>YJB@AIHv9Iu_uSiade8d{-plvR@$I$N{EfNx&X^i= z01EhD-rD~TY4BH{?f--~00u-R0o49~`>#BHM5>n&P2fS~m4BDpRxX=Mp$ON1md+p1 zW+yF3;QBKFNPRvjovPaSGx;o z%vXAF>jW?%2u6c6O>N8d>^+l@DJ@5m2AcW0`9h~zRBVFq_#dl}zb+@p9YyBhI|{mK zxnIK^Qj5+jv=WeVUWI@2MQ%rB=OJ`$>QhfJlkUq1B zgY5d{C%Zuhu)s>WDinEt&?OQ|4g~!w%)pX*H?;{kN8KidO}fXG=?`a@!~1N@9oTN# zrw{W)M7%Jj44E@M`Hx8F{GI+haJ9SK;iRPC%k5Q2c!RRHT0dH&;Wnzgem>CRPeERz z-HVX!yKj6B7@Y?DD&4Vmiuq4cmz}z9R}YAvF$@a?gaQzze@UH@wY7tlwS%GkA3*}j z6IZPA7?A=ml$V`&SBv(OLCxe!a2@AX&hCLV^g*b<{9q??d8z+oJ*33@5pw{WCCkf* z3-kdIGgjT0)?nap{o4iJN)K8!M_y6;+w+TQeLY>thX~zq%Jmgx+OweE2kQsJc*a6X zc@?$PuZ9YCb&c``&S2gGROB-2F>ui2s=4P#B;I7AfsB+X!9E8~QfxcEM6?C#myr3! zlxGk@^fM;a-UZ*-yMVIMU;-Gpdo$pnyAdn~KUf2c@Z=ms%D!97;$SI~$W%e)D@KTH zF;7PXsWXtc=uaDA4-v*SjELmz2n;}`+e+N&^?><^{Y0nW&2|Q;1pGcJEU70NAD5<# zxAb>~cQDB0w|@q?+FA04|3Ev?OxTFl&8)IOOxVHAn-Sa-V7iDwKdDBB5 z`Q4ArBre}y?Q>5geiVLnAdn}0Z8t9SKSW)%Zlh%N+iXEilZL$%@@DHn)yAOHUtt6~ zE?cpw2{D|rJ1)nPnunavSOnz>#Rl5e!|1tD$KTgmg`~wUN)?+h{>`)_)2J%dmyn(4 ziH(VYYPTg06^%QK67`a9A=KeXtp(&eHK@@n0 z6hB_kX_LLz&*zTCrpW1N-DF##Z|4D&54Jy_W*UN_dc)0g@%(hCslk?;L*|J=cys(^ ziqB0#NtLY4ewuImY}H{2XbyWa1yH+$1$)#;(1Gk zn)M&!h-fZ>*!g7`K@Bn)FN2Er3}$gu6hUP;U=0-DMao?AX%HJTg<%5oUtIF{qU%zG zaquWUb8TuM8DZ$tQ_b#Ia)H3?sncUX&}K_%&(O|c_TJ9JLlmA`1`_Ygp&X(}ldpmf zGYus+Z@iDS&Yhs|$@MN$8Jo_3A4SZy$Bh2$OoEZ=gKps%OgOvo`zrP~sY6HUru zyDLFfV$uqlA5P?I;GlMf#ahQkvc|F)dP2hw{ zsAt2CU99+1_u4=$-wbR8Pt6mnw+@gB3G=n#6eBwuXm&*l9RV8!+iUooXU;?PQ?&%RfkM3SZRX^wH{XuGHYLza5;j?fb7ZyZJY$` zaw)i$2}97fvwF^ZG#4g5U35C@kK?bPta&^jAEL!{K!GX<@ItdCRWL2tsXgv_ z^l${mXu7cr{TyziYEOt5ib#e#EPnY~0eQcQlP~ zHm}nOUN9D(i)Ws&07f$IFnoQDesitW<|OFmy!bf6$nmOtrmX0GiMOV4_RqnP+D)_t zut0%;xZr?*kp44}n;7aE7}_!YF^tzGj{PCxgBKY`oQ$ie!0*AmedDv=VxOmnSWPc# zkOGDH!b!dIi#qr>s-&mUKiG(+bsG!=hXnkT9uaGup1bp5F%~Wm6J&RMv!8G@5fX?u zN-KHUySlg7{w%gb+aqD%aQsDnPitbmvvoO0loVgan0ocV;Fw(PfQT=YG+y6vT%I@p zrTPnnFA4G>RgMXRfj`K^L#P%#c#c@N3~c&Yl^qScdCb`Eevl1A?z~eVRY|Y*^w6QTVX|E8R$%n+rrya?|h7pa*=LIM)J6 zH8ibp;u0ut=_lBs<+1`DVTs&e%Z`0X))ot;Dfppi>3g)=1R#`9j@@Luwma0>v zuOEQrwKIkEOA73?X|&SPC9BHED5 zlT=1stGYyKg@@Dpx*t!9j{8O(a=+_~<#PJnXd?vbChLbNM|1XWU4QfBeA>iNTKo)f zX;r{f;pfm7$||ZjkhB^I)6q&xWF1g1980^VK6Co)WtL^GD7r%jHdnDmP^U{x% z$VgA0$4sDF##5ab;A+axCNyzg@FE9W>PGlWFZyFS^k*;X zOB%DsP^tg30%=ipnBn=u;>miS5@JnXxBfg`!g0zxf_4q<2Nnpo?kck78x4O(|;+))+@ za0>CC0VY3tj}Njc5Jj!xA(u5MAXDhydazPHeemS3r;JUz+Ct7?YIHu5HMP+8eF8#;(p#ldPr;6ue*PJ>0`vIO2e2kgR|YLmpp*>gFAGc^K z1kNS+AT`w0+rxwb4_ctS6hK@qMB~Pt{oz|og-DN<=Y0hfLcNxO?#i2;0 zj)?ok`cuC*tIvLOXs@QI)7JL;@;+n^c8TOs>hx&qGM7-p3w|)SrTA4&A_kla@^|Uv&QfH(qE)8VQG>`9bACk#YXkMY}Swa?O92rwy63`;0^b2nGK}w1HgOW4Qx&YiV-(N>4VD-s@6D(qoM>VW>CzLe39r zR~CiK%pLdup%TI)3x_Rr%QMlC_R2okeZFf|YQE3!n7bJcBj-a z`o{$w(Baz^H6U;&Bry;W`hO-n2NOd}Lxz9Rf6aKOYGYxzAJIB7pLv3tIWw`nf;hUf zB;(c$me%rjDcyFl`%|Yr47Hkz<>VJ>!35OZPV!oF$4~Qu!nfZ>L6g=U5z30P5JXut zo2E)a8!lAM4_9Llas(19WbG1NbUeq4qOpXu0geMg zNzJaP6>Uff3G8+X*sw8@or+Kit}N=DmwfEl1?}mACe;+K zK$@Jvj|sKkk*L3wkK`ci*?Nxt%;p_z zxmx^PE8q*f_e{yE=|DmF9gbw<*})$8>RVt;{(Te zcpyAzVS)8&rW%kOPqMNH1)^l;)QEr2rWNi-?%6CmL;>sv`;PGqm<270Zyrs zjclV^<_h&_nknwY-}=?yF67x0fzC82Z9w{%1z9)}nzX1XA^At;L6Q(eborX`zIjZ< z`6xTRZ5PlXFp#Lkjt3UHn~PilyARt?w~WJ=%u#*K^mMsjGM*rK1-rMw zf(~1CO!hZyPTKH#I>?jmCgkyYxZYC!#>KFs{rdP6rtS5@dDoMDcejHnO~CVWKWxLx z*~I4oD?NT|kV6P>m9JPX&bZAP+QX$dBbw2omM!*G?zpQ5225?70W zP7eEFvCs~Q$ZsDwhCYZ$EE>oxavZrPc&&G_>f}o%^YD>`w;R?iQA6r3mUOs@8m#7E z_3g;Ckj(Q+)!4h{WKmzCXRwq+J{NBEq5`pA@)c+k5@Q2%AQKhILw2wQX;h+x8CE*% z(Ge0dj1chlD!F|c9em5joKbb9#)~*Z3*wY6dt5t2IW-}KBX^{3j(WQRQH24|9|z0= zL>$>YV!o*q0(S+3IyEzEPE5?JZ^yMB!i7X`-o+{4wfN8iV>eBtEWsATZnW%j7E!w9 z4!4*&$jfd}$j&gJ{#_Y}md>d-?Amc1$V26J_)8Y{psCfR&(PaYhuZ_&Dx(q6+C5fEcSe7$q$5Fn;b|CSA{_uZ@%|Kub-FbD9gS(V6GL`TG>op?j^$>uVohd^ z(p4|LEf?hZf|^iOo9pL&47l5uxk&s+a>wxsg&Nhh(;mxtR?uOP*YhM+{w5KG=r%#x z$*|D<+}x)PW!Bd58$o<~*+Gq2rf)HgGK1yfm~TKPcge!$Ax>XzQgrzu`(m@XE5Zoq zCp;Mw{aW$@LWLzvNzFwVzpYYiO#Kk% zoCWcO%B84#RFoI^242exy0T+IXhkupp~3 zF*pMeBG;5cDyEAyG@@_+nC`Kr+B%zEBvCDyH|L4lp(JWR@-e#a)JML*q~KOUciGYC z8a2*hphV$^VyQ2wM9a9sZsPmL5;8*#IHD-#Bz+&O4;+czTKAx$V!NNOC)>() z-S_;RBGNr++_1-tOsKzwm$%x{%j;!$@K+sA29SE7ffwoUVVoj~@w2IAUR!xLQ)=Mr zD21Naqkp_s^u&l@@R~P9i)HnFN{}1h$JS_s{aoN-Qkd4t{PJW(o?|zStKC|OA%|K} z-n;`LyHLpO)7Gxu(Px(N2(P4izAo`CL$PMXQL^nRLMFFCYZRbsRWJT7ENOP2f*hYn z!)L!hX1l)7Z8TeotEZ@PygvW1>|m!Y^e&TF1{)g@EpX4eck69>j*3HjL%y{{HFRGm z1`4ojVe$53Nny4e80zGpk6qMl@NQ{?`C6tZ*^O6ev*bhPUkmh8eT$E;lGgl!PbF!O z9elf7`XdF~dmDPs^?tn<%=tr1!tn;#+28`rQD zkLz31O+UKUi&ah)yW|BPAiTU&#AXKTRmOa>fzy{re5xe4eVk_?+@6C>6qm*&+5_cKx{0g{&EJb^YuUJgOz568S2}*LQZ;QfYai~iMtI8{-5s;NK!r>< zsahaJ7~BceHbo$2s-NAHHyWZ1yNhZWJt1dgf^7)) zfxvEgq{GCcgdn2*wqb7yasE1%<1A?hQNgQjJJE6YvNJvN~ zx|32!233I?iViGUeS#bE(1d5{rp^}?qnr}3ohb(z~L(7aQ%kh-Y9NS+{Fc#D$8^I$mKi5;wu_RII)x5fcHh^ z-bKWdU}CqAza;{mVgpkm>O?sl-oE~*n+ElQiwi-CS4AENn!mX<0ZHCJ}eKZs4O)6=faT_?A2=1jlYH zL#Qo?G_q5V-Qun4i@TnIyY1+GOX_K(6hmBQjNg3_jxSBn)VXM41X2d<)N|mZPU<(+ zV2*F?u39G-Ch5rVjLc=Ba+)U}5tp)jd_F<+UY5wwSS4>i;kvf_aw11Sq;ih+`UWChf7_9S}SZn6zSq77md7-B(k`R*=DoYPjnxE z&GafyAn#f<*oGv2R04`NzI=ridw5sbViRf!tZR>{Eo7dLSHLAMh;HNxiuGVI&Zn#b zg9I~e^Rc6l!%Ow(=%x%h=*4~dj{c4tpJN|>v6XyU=|Atg=3smb`S$#F9Uv@IvAQYE zK_$foRaiE3cQ`C$Hi;0@|F!n&9|7-Y>V*W5|GxvI~RKfKDITo zaW0xcEQiFC%Qi(QbkBN{l_rdUwA7;da{?p4>8a-qUstN)*x{s{!$uEh#gb^AvqNr}5U?u6Z313RBZhS3&!N_RqjfWu-XJcf`ceT}+(l8|LfQnwvD>;gul zw2j3ULzqr;hZe2hmKhw+j4B#D=EY^{DY4`4|i-k3ub zto9E>QZ*+XVdu-1D}p@b+H!Kzz|k3n#)#6&sx|*I|thX%1PdHNMgsw&8WFs|Ve8 zuF8r}LbAg;gD9PCV!7ErhAq@J!Buk&RxHsN_rB2ZftT`rzqlrbRZ+FWTaiZeY_{6+ z4YD=S-P*Y62SK9h9%rDwXVmc?`k=yuzJmod4z$hbzlkN51}BKteGCOP2C`iq8GXr> zkI62)Pt>hX>Om0uE!<#~|1(U{E=N~wdV#x58hVKKRcC)j>|_6+$9o{?fhYQ!kgWo@ zokW5Iqi4KldUtT;F)|8WdU!UCC`OG6ENA?wYdgcVWb3&c3Er*HqxsGWUb z4?$^Dc}smi)&|>12JIpR?RT?ED)6vo!rHo3L6=H|Ar00qO((nvMR+zM+g9ry2ttL+ zMfqc1ofa@cJnu7OGl(3bytZ+1(ghEwL zl;6i0Uh++m8c71O0ct-zR2^H4TUT<%CDx)Xf=i~DamG$g2H8&f8jmm8f(JPsSfnpn z4$`Pq%l=7%#avON(wsa8VwqrRh)$+TkBv;meKSt|EH;W+-j91ewDfm$ZpB$)kEYfQ z)lJY3G(;gMPHnKa)+42te#b8!HLlkepweXPl6IGp-OjM8!9kL}B8})Y<-SjZ{b)Y1 zi9M(Ln>ST*n*KhLqxHl>u^t+N8#SO|)&%zRV@urY7_yHUSYiDtERABZqwJdIQi2b-Xp$`VZXYuvNWbatVSrAQ*XDI7T!8C2>%NUK67R8g|0oMHfk zgpy)}jCfJu%w$SgY0Gz%ZJckNCF`=WrFtf)kXr+?KN%#7(jU;9QcEQchD-N9O%7N} zsxa1&($xhUv_91K7SJUl`Z_CGfNXjP((VBZ|#~@6VzBNXN+aO&gMJ4G(MSV zPH$eOnt_G7^R>raBcM%FTd>&<>vYlgg_2!hzJqoP+#ke=JFF?rM(EG>)NW8e;O=92 zPYR{*M@)PlrhR(BEg;-PPeP96u7qw?iG$)Yy0xfr>Ayi=FN--I{rI|^UY53tN-E71p|&M12F6rZia+eF~$r6h~5k=f1}L?m+cE=|q&^>55( zQ_W{`+g}j>X~7s?3bE}14+PZf0|W%9@b5EZdj}Vbe;**vW-Qt5G@wiT!E|?5y#r#RZqHz~aSTd@L)cH1-tx_wgNtjehAe z29gl5&JfE9fkS)dk_HVi2Yq#s?IJUE-iOMHw1~Mh3|b`17y~a)dT?RGsIZ^Q9P@XL zdX?Y5hPe~f47Q2rZ7a95q3-8)bD{F2pW*)~>>HS9M1Ml2{`CiW4Wo`X&lP$-Qqe z(}eO<85}q1vw57hEb&d_$919N^E@O}su;68OXGcYePy>CoYW3w{@Mq#!^?J@&#sk$ zksD<>aSvu9x*f*+v-W8vYCF+DP+ z>f6YJQS5XK!Lk`=;k31JHu-FseO5R7k^Jokz z{!pJa!J_Ks;(kQ8B%e9yU^8g?D#71TNB|AeZ4V`3l1Kp&vH0k~m&BS=Ev@;vfz#xN z-f_Q6oy}&N`SotN;7(WdGj(oUz6>YpP;CMn%6?Y~$u@<%^8$x$BH_8pXANbPZOvU> z5&Y^e&9+SaI9j-rqnFdXx`E0NFf471lNS48f@EVcg<`2B0STe=l9%WM(Rp-`Ct$fF5aG*2snK$|?y})jB4laE zfF)>iiK5@HOWifotkl?*3s3ETvI$@}sqih^{{&MBhy>YXwtk7!iaBD?%=>a!n3-}A zR?cREUZOlXE}D!9ySz0eRI^3U0PT&Nm_Oc=jepYR$1}87&RP^U+IX~g8EL7~+iS*2 z^XW`0#9>n0Wqeu^=l!vvFobfX1A%<)l5v-T)kZko7@EOd67ea7S+p{`Gjg6LmRXU8 z4e71u`4|M^BC{M4@sNm=_M3j=$y;#Mqx{eCl!Rlhy= z`c}X8&<>GwqRXL_5Bg*Z1Wh&z`ZZ(J_Uv9T`oxwu`ox_xeoOfFCl1mgXD@M;%~33v z{b3Y9fddo>nEhFx-pa`-onD3+0I8g#(uqt2!Wm7-`jqjGpRqTkQ}HU%t?`uN+kDLe zZog+dLlZ@xq=$TJ6*?7N;V#ABl4i0UE@Yk=y*q|{{W)K{uN!#U_dfcvpe@CJuwQT$ zA^IB2@zADPb5jHR309NiB-Nn#G|7)7mlW=klY@mN%NFI(e!Y2XAAvF}j+PLQe_s&| z0R$^k_z*44@!nJF6hh7{YZpwVoKm{{1#4rmY;b_&$&!d zTZ@24@=EhAjY9KGy>$1c;J22nw#LMAiCh-tQ5sz8)a`1;tV9SN;{7c%Z{rngm!nTZ zgJ@#iEgk)L=zo@&bNSuiJGscxtu9+6k<7(f(mSv22}a0VjeBTk(s|DxVX_!Kd3Nh3 z4hqDi_(D^#6gX+5AZr5Ob9xxr84Z$3&S`nvf&F+fLtdk?e^0#GZt~dim;ag8V%6V6 zB~`L8oN~`&JNJXxi2GjZqmjTxo4(Mk6t9s$rGx>RZ~-ZRofR{QD+Lt`NXGZgaPJW% z8G@wm*=>>yzG!I{Y&z&VLbqB7$;gMi0Z;GW2ibfb^gCEVE`NzPx^sF)5{6-1x#z*lTv2=wKI20F52v_(TJ2~2Rkd}PK^wCXtB(NJ3xMZ&MP zBEM|AriD^aLYTLSX3V5V-_KdOVZJXXwAHST9ExoYY~G@RTPiSLoQtP&A5RpgQC{-(Z7(5k3?WX6({xoXq-bY)}_I;wPE z)fL$X6~$YoM*TbHSx}!-rzCinW`etM=fnBb7sG2<*ecY_Sn55pQvogPl#=C@*v`on z{fERZ%{G~pjQyM34=qVlF$3x!v;K!Lr{O5m)u;R-uABZAeT#qGbxZ}26v>FP#AS)U zk?_I+Me&UP5+gvX1?+z#zrDnCJCZH}0We#XNUY0wCMMuOvn(9Wi(~76BZmqx%XG6D z{}|saJ}+tDq>@y-9`E7 zoqBHX+TU`j5M^yjB|>|W^iCOC-AI43XCGdPoo=z zI@1x($xzGXvQb(^Hm1^2i&;Rh+pOD#_1*lO_7GXOgq^$j^<6+_q}xP;MUNT}Yc%XWc8Y>`nN{LR|^q(y7Ei1+4%Ykl?H({Pn|_0&G79+9Wtwv)CrSFIL)Mo`&_ z0(w6}L5V}&b|WZOWBoGMrfkMH+QEw%Xk3VkCO@v%il%?=PQIXpkIz7EK%Hp9ipFX6 z&~Ozm6nEvS&YeV1q4Qj)ejOS5uj;kmTMLI(0v)(T^l$2UK)}gA4>s-y){U|uBwu6dWul&1oO=_e zqP95O-w&-cB8umta2e1*2WA9+M!jLd>8ry&BS7$2nOu351s=;k&v*gs!v9yT&k6IE zC=Gb>3@~p&`_C@T$xu)JFPi=IX<6%@4@f|p4`eTh@E2l^W)k{$hN?9OmOMaP%P2nP zK+10qerlSS+6D}JCT`l1ob0V}N=qMAD5l~K*vCX60`sB^>4z2$RtzMW4^ogNa(rUb zyJObwpG;@a@lScWUM`D9eDySPTtsm$R($0KaVJM5tHDvfD4DR`=$f0Crf}n}Knt%i zGd#$NdKKPqX~+0fM^UOM#`e-&=RxeexSM?iM~*NzIH^#32%gVD|1O9y9&lsgc*PpG zKQM~n6rH%5+dC}Xs~Z^VN}WG({bl+XQ>762hEu11E8eIa(l2ndnnWrJ^&L~T4U$OoV4iQ%WY_t&LjgX-ZIB}T=Bq05Z zskN1Zg07y0;UD_X(KvPMWkxjb4T>`!0T%|QEjiwRP_5|unxK$3pdF6BQihnQ-o1^B zMQB!ZeM@RfoS0zy)0?IDNDn738RlhM*q%d|)r%O8?bH;B!!xe;X00`eZ794s>;$13 z7x!JH4TcWS=9LM%JbZ!VO5MUmF8XvDV^NUu%}~CHcny7HtMXg~o-=mY*$#z0c$U(9 zDGPtJ^W=R0@Er(!9zG@)dMq#IaygCG=qwfb7(iFf^+&b*iM+hNSY8)s3$$7r%O)+G zGm-p$36c+13n6jt+LU4>2pn+afubhX0V1>pbOf$5`j~^OKh20TYGE%hAHnhkK)2nk znCN(w9QpG7W=rY7N1kp+73Dv$CCAm{OKT9{_FJHj=woLXHq;h9k&GBE{DAkvVt z^nR!|7Ph^iJn{)2igwn#V+lv)iAJ9Vc?fp>P}2KJk&i%Hb`jRQB@ZnUz34G=+rKHG z1-1`^lX@}QpjPDB@P^D*UPUCBkeP4RMf$3rL8s!p<)(M$R`|YnsH++C<2E*+YZHolQc>)D z`kwFRRN|!@vc}+W{Jk)4Wg(WWkU<;+=Jp^~a)U;7#=D>4AMIy4gZaBf0Aqme8+!(QYrB6*A|QX}|93C}{B-HDvgW-X&;m9fUl6+=1mgIhiK?q2zIvIwS#PK*{;D~1zA%w*xm@$*Om?D~@PL5Mm~&1*y-Es4Gl!Lk5@akQ+Ep{hkUPpA z?bAe^PQ;W<4mSsD-h&gFHLMkFo63yu9f}5;$TE7{_sM~mhOWgcc*_0Q&B?DC*k#Fx z!mR^zJK7fT93%#z=+5NH#!Zj|XCeZ^`I{s)Zw+HWNO=69Fm+BGl5+0%u2sWpcv$hU zMX{b6J(4G)cx#*#!uku2q93u#iM&F2Fea>fORHx+l<|=0z!{pr_ah3#@y!c);~%$c z7^-x~8Tb}xptp6UPa>cl1uH+iNr(JAehUuxe!3BAE3vGTqJ_Abzs| z@k9En`03i%{4oyxf9V5=-XGE@Cn*icUfND_MOeD7x(qVItA&KahxZt(w24P%g&RXI zi-Pp>&=j9QY_)5^v%)aJ;1d?LZg7E>`|VpsQW){Xq*zg%<-(J^hezFTZL+^&4@cOE zdk|CSW)S2hR{CHx>p(cAg~oxV*n4_4CFiN2hRl*&fo%?E$n+%h0;}3~LYy`Ca$jk# z3iGRo33@3B5Ar_A&Xe0SLy4W;Bxcr02E`Wyw)^#9|5wjsR@gl`L<_#0Z=%U+4?kdi zi@*p3!(uAEBY@kQvWXd_m5aItu-G%8_{eLblMHQz+F8Kp+dry>3fy0F()oo|h%WUe z?OW&ov0DQ#>xW&_9oFIIkIFaAC9F~%U25(Qj+n`O+N(}7lZO2TcTtY+MF?YYNZ^eOoB6xxySMr!DS#c#26;Q=j-g7@vg>UfB&7~tpMg{tK z%iax0x>jx}d*=3z0fCz}C_Ox~mz#aFw?dFNRTpWAQI2Kz9yUmHj1u@$bOjQ@Q?v?m+xQV%Oi{fA=l?1qK3&5%?4SfAKW@ zozw6BV!wDn3jfLJZ{B0S<9~O>`Gr3f{S*IJ$DH5czk50Sf=^2P3IB`F!|&+dt-F7r zF=YPo@_*WT|Bn9M==2x5UH(t>?qs)zId5`0pp^zudYo8Q5|?;n1Fuk8K=|F)a>ox|@7;9nf<9RB2BWc`=r@bCD) zuUdYgfq>q=0RjEb8s>NS-)Eh_!rfi|0{?Rkk`V_7^sGPTCnTU(K<8w3|KsZa08Wi0 AmH+?% literal 0 HcmV?d00001 diff --git a/INSIGHT_Konzept_v1.0.docx b/INSIGHT_Konzept_v1.0.docx new file mode 100644 index 0000000000000000000000000000000000000000..71b2ebf8e1d8ff239c80e95ad1ff425d5456da32 GIT binary patch literal 141557 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≧Q6zP(N{Np0;gL-#Y;* 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 literal 0 HcmV?d00001 diff --git a/README.md b/README.md index 6aa298a..a48c4ac 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,206 @@ -# INSIGHT-MVP +# INSIGHT MVP +Erweiterbare, mandantenfaehige SaaS-Business-Plattform der Xinion IT GmbH. + +--- + +## Inhaltsverzeichnis + +- [Projektuebersicht](#projektuebersicht) +- [Voraussetzungen](#voraussetzungen) +- [Setup (Entwicklungsumgebung)](#setup-entwicklungsumgebung) +- [Services & Ports](#services--ports) +- [Projektstruktur](#projektstruktur) +- [Branching & Commits](#branching--commits) +- [Dokumentation](#dokumentation) + +--- + +## Projektuebersicht + +INSIGHT ist eine Infrastruktur-Shell, auf die fachliche Module (erstes Modul: CRM) als isolierte Docker-Container aufgesetzt werden. Das System ist Cloud-Native und Kubernetes-ready. + +**Kernprinzipien:** +- Zero-Trust (mTLS intern) +- Stateless Backend-Services +- Separate Datenbank pro Mandant (Tenant-Isolation) +- Provider-Modell fuer Authentifizierung (lokal + MS SSO) + +**Tech Stack:** +TypeScript | NestJS | React + Vite | PostgreSQL | Prisma | Redis | Traefik | Docker + +--- + +## Voraussetzungen + +### Fuer lokale Entwicklung (MacBook) +- Git mit SSH-Zugang zu `git.xinion.lan` +- Docker Desktop oder Docker Engine +- Node.js >= 20 LTS +- npm oder yarn + +### Fuer den Server (ProxmoxVE VM) +- Ubuntu 24.04 LTS +- Docker Engine + Compose Plugin (kein Docker Desktop) +- SSH-Key aus `.keys/deploy_ed25519.pub` im `authorized_keys` des `deploy`-Users + +--- + +## Setup (Entwicklungsumgebung) + +### 1. Repository klonen +```bash +git clone ssh://git@git.xinion.lan/gitadmin/INSIGHT-MVP.git +cd INSIGHT-MVP +``` + +### 2. Environment konfigurieren +```bash +cp .env.example .env +# .env oeffnen und alle Werte befuellen (Passwoerter, Keys, etc.) +``` + +### 3. JWT-Schluessel generieren +```bash +# RS256 Schluessel fuer JWT-Signierung +mkdir -p packages/core-service/keys +openssl genpkey -algorithm RSA -out packages/core-service/keys/jwt-private.pem -pkeyopt rsa_keygen_bits:2048 +openssl rsa -pubout -in packages/core-service/keys/jwt-private.pem -out packages/core-service/keys/jwt-public.pem +``` + +### 4. Services starten +```bash +# Basis-Services +docker compose up -d + +# Mit Observability-Stack +docker compose -f docker-compose.yml -f docker-compose.observability.yml up -d +``` + +### 5. Datenbank-Migration +```bash +# Core-Schema +docker compose exec core npx prisma migrate deploy --schema=./prisma/core.schema.prisma + +# Tenant-Schema (wird beim Onboarding automatisch ausgefuehrt) +``` + +### 6. Health-Checks pruefen +```bash +curl http://localhost:3000/health # Core-Service +curl http://localhost:8080 # Frontend +``` + +### 7. Erster Login +- URL: https://insight-dev.xinion.lan (oder http://localhost) +- Initialer Admin-Account wird beim ersten Start via Seed-Script angelegt + +--- + +## Services & Ports + +| Service | Port (intern) | URL (extern via Traefik) | Beschreibung | +|---------------|---------------|----------------------------------|------------------------| +| Traefik | 80/443 | https://insight-dev.xinion.lan | API Gateway | +| Core-Service | 3000 | /api/v1/* | NestJS Backend | +| Frontend | 8080 | /* | React App | +| PostgreSQL | 5432 | - | Datenbank | +| PgBouncer | 6432 | - | Connection Pooler | +| Redis | 6379 | - | Cache & Event Bus | +| step-ca | 9000 | - | Interne CA (mTLS) | +| Grafana | 3001 | SSH-Tunnel | Monitoring Dashboards | + +--- + +## Projektstruktur + +``` +INSIGHT-MVP/ + docker-compose.yml # Basis-Services + docker-compose.observability.yml # Monitoring-Stack + .env.example # Alle Umgebungsvariablen (keine Werte!) + .gitignore + README.md # <- Du bist hier + + .keys/ # SSH Deployment Keys + deploy_ed25519 + deploy_ed25519.pub + + docs/ # Projektdokumentation + INFRASTRUCTURE.md # Server & VM Konfiguration + ACCESS.md # Zugangsdaten & SSH-Infos + + packages/ + core-service/ # NestJS Backend + src/ + core/ + auth/ # Auth-Service (Provider-Modell) + users/ # User-Verwaltung + tenants/ # Tenant-Verwaltung + modules/ # Module-Registry + common/ + guards/ # JwtGuard, RolesGuard, ScopeGuard + decorators/ # @Public(), @Roles(), @RequireScope() + filters/ # GlobalExceptionFilter + interceptors/ # Logging, Response-Transformation + config/ # Env-Validierung (class-validator) + prisma/ # PrismaService + TenantPrismaService + prisma/ + core.schema.prisma # platform_core Tabellen + tenant.schema.prisma # Tenant-DB Tabellen + + frontend/ # React + Vite + src/ + shell/ # App-Shell (Layout, Routing) + auth/ # Login, 2FA, Token-Management + admin/ # Admin-Bereich + components/ # Shared UI-Komponenten + + config/ # Service-Konfigurationen + traefik/ + prometheus/ + step-ca/ + + .forgejo/ + workflows/ # CI/CD Pipelines + ci.yml + develop.yml + release.yml +``` + +--- + +## Branching & Commits + +### Branching-Strategie: GitFlow + +| Branch | Zweck | +|------------- |------------------------------------------| +| `main` | Produktion (nur via Merge, geschuetzt) | +| `develop` | Integration (nur via Merge, geschuetzt) | +| `feature/*` | Neue Features | +| `fix/*` | Bugfixes | +| `hotfix/*` | Kritische Fixes auf main | + +### Commit-Format: Conventional Commits +``` +feat: Neues Feature +fix: Bugfix +chore: Tooling, Dependencies +docs: Dokumentation +refactor: Refactoring ohne Funktionsaenderung +``` + +--- + +## Dokumentation + +| Dokument | Beschreibung | +|---------------------------------|-------------------------------------------| +| `README.md` | Dieses Dokument (Onboarding) | +| `docs/INFRASTRUCTURE.md` | Server-Infrastruktur & VM-Setup | +| `docs/ACCESS.md` | Zugangsdaten & SSH-Verbindungen | +| `INSIGHT_Konzept_v1.0.docx` | Vollstaendiges Konzeptdokument (23 Kap.) | +| `CLAUDE_BRIEFING.docx` | Entwickler-Briefing (Kurzreferenz) | +| `Summarize.md` | Aenderungsprotokoll (aktueller Stand) | +| `RUNBOOK.md` | Disaster Recovery Anleitung (folgt) | diff --git a/Summarize.md b/Summarize.md new file mode 100644 index 0000000..ccab691 --- /dev/null +++ b/Summarize.md @@ -0,0 +1,66 @@ +# INSIGHT MVP - Aenderungsprotokoll + +## Stand: 2026-03-08 + +### Aktueller Sprint: Sprint 1 (Alpha) + +--- + +### Aenderungen in dieser Session + +#### Projektinitialisierung & Infrastruktur-Definition + +**Was wurde gemacht:** + +1. **SSH Deployment Key erstellt** + - Ed25519-Schluessel unter `.keys/deploy_ed25519` generiert + - Public Key muss auf dem Entwicklungsserver und in Forgejo hinterlegt werden + - Key-Kommentar: `insight-deploy@xinion.lan` + +2. **Infrastruktur-Definition erstellt** (`docs/INFRASTRUCTURE.md`) + - ProxmoxVE VM-Spezifikation: Ubuntu 24.04 LTS, 4 vCPU, 8 GB RAM, 60 GB SSD + - Docker-Netzwerk-Architektur mit 3 isolierten Netzwerken + - Komplette Service-Landschaft definiert (Traefik, Core, Frontend, PostgreSQL, PgBouncer, Redis, step-ca) + - Observability-Stack definiert (Prometheus, Grafana, Loki, Tempo, Promtail, cAdvisor) + - Schritt-fuer-Schritt VM-Setup Anleitung + +3. **Zugangsdaten-Dokument erstellt** (`docs/ACCESS.md`) + - Git Repository Zugangsdaten + - SSH-Key Dokumentation und Verwendung + - Server-Zugangsdaten (Platzhalter fuer IP) + - Alle Service-Ports dokumentiert + - Deployment-Pfad dokumentiert + - Wichtige Befehle (MacBook & Server) + +4. **Projektstruktur aufgesetzt** + - Verzeichnisstruktur gemaess Briefing angelegt + - packages/core-service/ (NestJS Backend) + - packages/frontend/ (React + Vite) + - config/ (Traefik, Prometheus, step-ca) + - .forgejo/workflows/ (CI/CD) + +5. **Basis-Konfigurationsdateien erstellt** + - `.gitignore` - alle relevanten Ausschluesse + - `.env.example` - alle Umgebungsvariablen dokumentiert (ohne Werte) + - `README.md` - vollstaendiges Onboarding-Dokument + +--- + +### Naechste Schritte + +- [ ] VM in ProxmoxVE erstellen und konfigurieren +- [ ] SSH Deploy Key auf Server und in Forgejo hinterlegen +- [ ] `docker-compose.yml` erstellen (alle Basis-Services) +- [ ] `docker-compose.observability.yml` erstellen +- [ ] NestJS Core-Service implementieren (Auth, Users, Tenants) +- [ ] Prisma-Schemas erstellen (core + tenant) +- [ ] React Frontend-Shell implementieren +- [ ] CI/CD Pipelines in Forgejo Actions definieren + +--- + +### Offene Fragen / Abhaengigkeiten + +- Server-IP wird erst bei VM-Erstellung vergeben +- DNS-Eintraege (insight-dev.xinion.lan) muessen konfiguriert werden +- Forgejo Deploy Key muss manuell hinterlegt werden diff --git a/config/prometheus/.gitkeep b/config/prometheus/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/config/step-ca/.gitkeep b/config/step-ca/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/config/traefik/.gitkeep b/config/traefik/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/ACCESS.md b/docs/ACCESS.md new file mode 100644 index 0000000..69b8998 --- /dev/null +++ b/docs/ACCESS.md @@ -0,0 +1,202 @@ +# INSIGHT MVP - Zugangsdaten & Server-Zugriff + +> **Dieses Dokument wird laufend aktualisiert und enthaelt alle relevanten +> Zugangsinformationen fuer das Projekt.** + +--- + +## 1. Git Repository + +| Parameter | Wert | +|------------------|-----------------------------------------------------| +| Git-Server | Forgejo (self-hosted) | +| URL | `git.xinion.lan` | +| Repository (SSH) | `ssh://git@git.xinion.lan/gitadmin/INSIGHT-MVP.git` | +| Repository (HTTP)| `https://git.xinion.lan/gitadmin/INSIGHT-MVP` | +| Organisation | `gitadmin` | +| Zugriff | SSH Key-basiert | +| CI/CD | Forgejo Actions (GitHub Actions kompatibel) | +| Container Registry | `git.xinion.lan` (Forgejo built-in) | + +--- + +## 2. SSH Deployment Key + +Der Deployment Key liegt im Repository unter `.keys/`: + +| Datei | Beschreibung | +|------------------------------|-----------------------| +| `.keys/deploy_ed25519` | Private Key (Ed25519) | +| `.keys/deploy_ed25519.pub` | Public Key | + +### Public Key (zur Hinterlegung auf Servern) +``` +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMuTpqzLyjqTIDMJ4bwEE4o2JeHH3imL+NeipeuBfiTo insight-deploy@xinion.lan +``` + +### SSH-Verbindung zum Server +```bash +# Verbindung zum Entwicklungsserver: +ssh -i .keys/deploy_ed25519 deploy@ + +# Mit SSH-Config (empfohlen): +# Eintrag in ~/.ssh/config: +Host insight-dev + HostName + User deploy + IdentityFile ~/git.xinion.lan/INSIGHT-MVP/.keys/deploy_ed25519 + StrictHostKeyChecking accept-new +``` + +### Wo der Public Key hinterlegt werden muss +1. **Entwicklungsserver (VM)**: `/home/deploy/.ssh/authorized_keys` +2. **Forgejo**: Repository Settings > Deploy Keys (fuer CI/CD) + +--- + +## 3. Entwicklungsserver (ProxmoxVE VM) + +| Parameter | Wert | +|------------------|-----------------------------------------| +| **Hostname** | `insight-dev-01` | +| **OS** | Ubuntu 24.04 LTS | +| **IP** | _wird bei VM-Erstellung vergeben_ | +| **SSH-Port** | 22 | +| **SSH-User** | `deploy` | +| **SSH-Key** | `.keys/deploy_ed25519` | +| **Docker** | Docker Engine + Compose Plugin | +| **Projekt-Pfad** | `/home/deploy/insight/` | + +### Schnellzugriff nach VM-Setup +```bash +# SSH auf den Server +ssh -i .keys/deploy_ed25519 deploy@ + +# Status aller Container pruefen +docker compose ps + +# Logs eines Services +docker compose logs -f core + +# Neustart aller Services +docker compose restart + +# Nur Backend neustarten +docker compose restart core +``` + +--- + +## 4. Service-Ports (auf der VM) + +| Service | Interner Port | Externer Port | URL | +|-----------------|---------------|---------------|----------------------------------| +| Traefik (HTTP) | 80 | 80 | http://insight-dev.xinion.lan | +| Traefik (HTTPS) | 443 | 443 | https://insight-dev.xinion.lan | +| Traefik Dashboard | 8080 | - | Nur intern | +| Core-Service | 3000 | - | Via Traefik: /api/v1/* | +| Frontend | 8080 | - | Via Traefik: /* | +| PostgreSQL | 5432 | - | Nur intern (Docker-Netzwerk) | +| PgBouncer | 6432 | - | Nur intern (Docker-Netzwerk) | +| Redis | 6379 | - | Nur intern (Docker-Netzwerk) | +| step-ca | 9000 | - | Nur intern (Docker-Netzwerk) | + +### Observability (nur intern, kein oeffentlicher Zugriff) + +| Service | Port | Zugriff | +|-----------------|-------|----------------------------------| +| Grafana | 3001 | SSH-Tunnel: `ssh -L 3001:localhost:3001 deploy@` | +| Prometheus | 9090 | Nur intern | +| Loki | 3100 | Nur intern | +| Tempo | 3200 | Nur intern | + +--- + +## 5. Datenbank-Zugangsdaten + +> **Echte Passwoerter stehen in der `.env`-Datei auf dem Server. +> Niemals in Git committen!** + +| Parameter | Wert (Platzhalter) | +|-------------------|---------------------------------| +| DB-Host | `pgbouncer` (via Docker-Netzwerk) | +| DB-Port | `6432` | +| Core-DB-Name | `platform_core` | +| Tenant-DB-Schema | `tenant_{slug}` | +| DB-User | Siehe `.env` -> `DB_USER` | +| DB-Passwort | Siehe `.env` -> `DB_PASSWORD` | + +--- + +## 6. Container Registry + +| Parameter | Wert | +|------------------|-----------------------------------------------------| +| Registry-URL | `git.xinion.lan` | +| Image-Prefix | `git.xinion.lan/gitadmin/insight-{service}` | +| Authentifizierung| Forgejo Login-Credentials | + +### Image-Namen +``` +git.xinion.lan/gitadmin/insight-core:latest +git.xinion.lan/gitadmin/insight-core:develop +git.xinion.lan/gitadmin/insight-core:v0.1.0 +git.xinion.lan/gitadmin/insight-frontend:latest +``` + +--- + +## 7. Deployment-Pfad + +``` +MacBook (Entwicklung) + | + | git push + v +Forgejo (git.xinion.lan) + | + | Forgejo Actions CI/CD + | - Lint, Type-Check, Tests, Build + | - Docker Image bauen & pushen + v +Server (insight-dev-01) + | + | docker compose pull && docker compose up -d + v +Laufende Anwendung +``` + +--- + +## 8. Wichtige Befehle + +### Vom MacBook aus +```bash +# Code pushen +git push origin develop + +# SSH auf Server +ssh -i .keys/deploy_ed25519 deploy@ + +# Grafana oeffnen (SSH-Tunnel) +ssh -L 3001:localhost:3001 -i .keys/deploy_ed25519 deploy@ +# Dann im Browser: http://localhost:3001 +``` + +### Auf dem Server +```bash +# Alle Services starten +docker compose up -d + +# Mit Observability +docker compose -f docker-compose.yml -f docker-compose.observability.yml up -d + +# Health-Check +curl http://localhost:3000/health + +# Datenbank-Migration +docker compose exec core npx prisma migrate deploy + +# Logs folgen +docker compose logs -f --tail=100 +``` diff --git a/docs/INFRASTRUCTURE.md b/docs/INFRASTRUCTURE.md new file mode 100644 index 0000000..71d2c44 --- /dev/null +++ b/docs/INFRASTRUCTURE.md @@ -0,0 +1,257 @@ +# INSIGHT MVP - Infrastruktur-Definition + +## 1. Uebersicht + +Die gesamte INSIGHT-Plattform laeuft auf einer ProxmoxVE-VM im internen Netzwerk. +Alle Services werden als Docker-Container betrieben. + +--- + +## 2. VM-Konfiguration (ProxmoxVE) + +| Komponente | Spezifikation | +|-----------------|----------------------------------------| +| **Hostname** | `insight-dev-01` | +| **OS** | Ubuntu 24.04 LTS (Server) | +| **CPU** | 4 vCPUs | +| **RAM** | 8 GB (16 GB empfohlen) | +| **Storage** | 60 GB SSD | +| **Netzwerk** | Feste interne IP (wird bei Setup vergeben) | +| **SSH-Zugang** | Key-basiert (Ed25519), kein Passwort-Login | +| **User** | `deploy` (non-root, Mitglied der `docker`-Gruppe) | + +### Betriebssystem-Hardening + +- SSH: nur Key-basiert (`PasswordAuthentication no`) +- Firewall (ufw): + - Port 22 (SSH) - nur internes Netzwerk + - Port 80 (HTTP -> Redirect auf HTTPS) + - Port 443 (HTTPS) + - Alle anderen Ports: DENY +- Automatische Sicherheitsupdates: `unattended-upgrades` aktiviert +- Fail2ban fuer SSH-Brute-Force-Schutz + +--- + +## 3. Software auf der VM + +| Software | Version | Installationsmethode | +|---------------------|-------------|--------------------------------| +| Docker Engine | >= 27.x | Official Docker APT Repository | +| Docker Compose | Plugin | Mitgeliefert mit Docker Engine | +| Git | >= 2.x | APT | +| ufw | Aktuell | APT (vorinstalliert) | +| fail2ban | Aktuell | APT | +| unattended-upgrades | Aktuell | APT (vorinstalliert) | + +**Kein** Docker Desktop, kein Node.js, kein npm auf der VM. +Alles laeuft in Containern. + +--- + +## 4. Docker-Netzwerk-Architektur + +``` + Internet / Internes Netz + | + [ Port 80/443 ] + | + +-------v--------+ + | Traefik | API Gateway, SSL-Terminierung, + | (Gateway) | Rate Limiting, mTLS-Terminierung + +---+-------+----+ + | | + +---------+ +---------+ + | | + +-------v--------+ +-------v--------+ + | Core-Service | | Frontend | + | (NestJS) | | (React/Vite) | + | Port: 3000 | | Port: 8080 | + +---+--------+----+ +----------------+ + | | + +-----v--+ +--v------+ + | Redis | | PgBouncer| + | :6379 | | :6432 | + +----+----+ +----+-----+ + | | + | +----v------+ + | | PostgreSQL | + | | :5432 | + +-------+------------+ +``` + +### Docker-Netzwerke + +| Netzwerk | Zweck | +|---------------|-------------------------------------------------| +| `insight-web` | Traefik <-> Core-Service, Frontend (extern erreichbar) | +| `insight-db` | Core-Service <-> PgBouncer <-> PostgreSQL (intern) | +| `insight-cache`| Core-Service <-> Redis (intern) | + +### mTLS (step-ca) + +Alle interne Kommunikation zwischen Containern wird ueber mTLS abgesichert. +step-ca (Smallstep) fungiert als interne Certificate Authority. + +| Komponente | Zertifikat | +|---------------|-------------------------------| +| Traefik | Wildcard fuer externe Domain | +| Core-Service | `core-service.insight.local` | +| Frontend | `frontend.insight.local` | +| PostgreSQL | `postgres.insight.local` | +| Redis | `redis.insight.local` | +| PgBouncer | `pgbouncer.insight.local` | + +--- + +## 5. Container-Services (docker-compose.yml) + +| Service | Image | Port (intern) | Port (extern) | Beschreibung | +|---------------|--------------------------------|---------------|---------------|-------------------------------| +| `traefik` | traefik:3 | 80, 443, 8080 | 80, 443 | API Gateway, Reverse Proxy | +| `core` | insight-core:latest | 3000 | - | NestJS Backend | +| `frontend` | insight-frontend:latest | 8080 | - | React App (Nginx served) | +| `postgres` | postgres:16-alpine | 5432 | - | Datenbank | +| `pgbouncer` | edoburu/pgbouncer:latest | 6432 | - | Connection Pooler | +| `redis` | redis:7-alpine | 6379 | - | Cache, Sessions, Event Bus | +| `step-ca` | smallstep/step-ca:latest | 9000 | - | Interne Certificate Authority | + +--- + +## 6. Observability-Stack (docker-compose.observability.yml) + +| Service | Image | Port (intern) | Beschreibung | +|------------------|---------------------------------|---------------|-----------------------------| +| `prometheus` | prom/prometheus:latest | 9090 | Metrics-Storage | +| `grafana` | grafana/grafana:latest | 3001 | Dashboards & Alerting | +| `loki` | grafana/loki:latest | 3100 | Log-Storage | +| `tempo` | grafana/tempo:latest | 3200, 4317 | Tracing-Backend | +| `promtail` | grafana/promtail:latest | - | Log-Collector | +| `cadvisor` | gcr.io/cadvisor/cadvisor:latest | 8081 | Container-Metrics | +| `postgres-exp` | prometheuscommunity/postgres-exporter | 9187 | DB-Metrics | + +**Grafana ist NICHT oeffentlich erreichbar** - nur ueber SSH-Tunnel oder internes Netz. + +--- + +## 7. Datenbank-Struktur + +``` +PostgreSQL-Server + platform_core <- Einmalig: Tenants, Users, Roles, Modules, Help + tenant_{slug} <- Pro Mandant (z.B. tenant_acme_corp) +``` + +| Datenbank | Zweck | +|-----------------|-----------------------------------------------------| +| `platform_core` | Plattform-Verwaltung (Users, Tenants, Roles, Modules) | +| `tenant_{slug}` | Mandant-Daten (Profile, Stammdaten, Moduldaten) | + +--- + +## 8. DNS / Domains + +| Eintrag | Ziel | Zweck | +|----------------------------|--------------------|-------------------------------| +| `insight-dev.xinion.lan` | VM-IP | Entwicklungs-Frontend | +| `api.insight-dev.xinion.lan` | VM-IP | API-Endpunkt | +| `git.xinion.lan` | Forgejo-Server | Git Repository & CI/CD | + +--- + +## 9. Backup (Alpha/Dev) + +| Was | Wohin | Frequenz | +|----------------------|----------------------------------------|-----------| +| PostgreSQL (alle DBs)| Separates ProxmoxVE Volume | Taeglich | +| Media-Dateien | Separates ProxmoxVE Volume | Taeglich | +| Konfiguration | Git Repository (ohne .env) | Per Commit| + +--- + +## 10. VM-Setup Anleitung (Schritt fuer Schritt) + +### 10.1 VM in ProxmoxVE erstellen +```bash +# ProxmoxVE Web-UI oder CLI: +# - Template: Ubuntu 24.04 LTS Cloud-Init +# - CPU: 4 Cores +# - RAM: 8192 MB +# - Disk: 60 GB (SCSI, SSD-backed) +# - Network: vmbr0, DHCP oder feste IP +``` + +### 10.2 Basis-Setup nach Erstinstallation +```bash +# System aktualisieren +sudo apt update && sudo apt upgrade -y + +# Deploy-User anlegen +sudo adduser --disabled-password deploy +sudo usermod -aG sudo deploy + +# SSH-Key fuer Deploy-User hinterlegen +sudo mkdir -p /home/deploy/.ssh +sudo cp /path/to/deploy_ed25519.pub /home/deploy/.ssh/authorized_keys +sudo chown -R deploy:deploy /home/deploy/.ssh +sudo chmod 700 /home/deploy/.ssh +sudo chmod 600 /home/deploy/.ssh/authorized_keys + +# SSH haerten +sudo sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config +sudo sed -i 's/PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config +sudo systemctl restart sshd +``` + +### 10.3 Firewall +```bash +sudo ufw default deny incoming +sudo ufw default allow outgoing +sudo ufw allow 22/tcp # SSH +sudo ufw allow 80/tcp # HTTP +sudo ufw allow 443/tcp # HTTPS +sudo ufw enable +``` + +### 10.4 Docker installieren +```bash +# Docker Official GPG Key +sudo apt install -y ca-certificates curl +sudo install -m 0755 -d /etc/apt/keyrings +sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ + -o /etc/apt/keyrings/docker.asc +sudo chmod a+r /etc/apt/keyrings/docker.asc + +# Docker Repo hinzufuegen +echo "deb [arch=$(dpkg --print-architecture) \ + signed-by=/etc/apt/keyrings/docker.asc] \ + https://download.docker.com/linux/ubuntu \ + $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ + sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + +# Docker installieren +sudo apt update +sudo apt install -y docker-ce docker-ce-cli containerd.io \ + docker-buildx-plugin docker-compose-plugin + +# Deploy-User zur docker-Gruppe +sudo usermod -aG docker deploy +``` + +### 10.5 Fail2ban +```bash +sudo apt install -y fail2ban +sudo systemctl enable fail2ban +sudo systemctl start fail2ban +``` + +### 10.6 Projekt deployen +```bash +# Als deploy-User: +su - deploy +git clone git@git.xinion.lan:gitadmin/INSIGHT-MVP.git ~/insight +cd ~/insight +cp .env.example .env +# .env befuellen mit echten Werten +docker compose up -d +``` diff --git a/packages/core-service/prisma/.gitkeep b/packages/core-service/prisma/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/core-service/src/common/decorators/.gitkeep b/packages/core-service/src/common/decorators/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/core-service/src/common/filters/.gitkeep b/packages/core-service/src/common/filters/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/core-service/src/common/guards/.gitkeep b/packages/core-service/src/common/guards/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/core-service/src/common/interceptors/.gitkeep b/packages/core-service/src/common/interceptors/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/core-service/src/config/.gitkeep b/packages/core-service/src/config/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/core-service/src/core/auth/.gitkeep b/packages/core-service/src/core/auth/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/core-service/src/core/modules/.gitkeep b/packages/core-service/src/core/modules/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/core-service/src/core/tenants/.gitkeep b/packages/core-service/src/core/tenants/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/core-service/src/core/users/.gitkeep b/packages/core-service/src/core/users/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/core-service/src/prisma/.gitkeep b/packages/core-service/src/prisma/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/frontend/public/.gitkeep b/packages/frontend/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/frontend/src/admin/.gitkeep b/packages/frontend/src/admin/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/frontend/src/auth/.gitkeep b/packages/frontend/src/auth/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/frontend/src/components/HelpPanel/.gitkeep b/packages/frontend/src/components/HelpPanel/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/frontend/src/components/HelpTooltip/.gitkeep b/packages/frontend/src/components/HelpTooltip/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/frontend/src/shell/.gitkeep b/packages/frontend/src/shell/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/templates/cv/default/.gitkeep b/templates/cv/default/.gitkeep new file mode 100644 index 0000000..e69de29 From 0e052b001cb48a4d0290df178815347472468ef2 Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Sun, 8 Mar 2026 10:34:01 +0100 Subject: [PATCH 02/50] chore: add CI/CD SSH key and update ACCESS.md with both keys - Generate separate Ed25519 key for Forgejo Actions CI/CD pipeline - Document both keys with clear purpose separation: deploy_ed25519 = server access (manual/Claude) cicd_ed25519 = automated deployments (Forgejo Actions) - Add key placement matrix (which key goes where) Co-Authored-By: Claude Opus 4.6 --- .keys/cicd_ed25519 | 7 ++++++ .keys/cicd_ed25519.pub | 1 + docs/ACCESS.md | 51 +++++++++++++++++++++++++++++++++--------- 3 files changed, 48 insertions(+), 11 deletions(-) create mode 100644 .keys/cicd_ed25519 create mode 100644 .keys/cicd_ed25519.pub diff --git a/.keys/cicd_ed25519 b/.keys/cicd_ed25519 new file mode 100644 index 0000000..250c361 --- /dev/null +++ b/.keys/cicd_ed25519 @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACDZT6PgLwzEzGQtBuPaPpLlPfP2gvOTfdEFN2vhWk46BgAAAKC7x6Lou8ei +6AAAAAtzc2gtZWQyNTUxOQAAACDZT6PgLwzEzGQtBuPaPpLlPfP2gvOTfdEFN2vhWk46Bg +AAAECBB/Q1ujr07L/3IwgTE3siUvM5fBLMO5iuw5eHkR1VctlPo+AvDMTMZC0G49o+kuU9 +8/aC85N90QU3a+FaTjoGAAAAF2luc2lnaHQtY2ljZEB4aW5pb24ubGFuAQIDBAUG +-----END OPENSSH PRIVATE KEY----- diff --git a/.keys/cicd_ed25519.pub b/.keys/cicd_ed25519.pub new file mode 100644 index 0000000..a628060 --- /dev/null +++ b/.keys/cicd_ed25519.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINlPo+AvDMTMZC0G49o+kuU98/aC85N90QU3a+FaTjoG insight-cicd@xinion.lan diff --git a/docs/ACCESS.md b/docs/ACCESS.md index 69b8998..6de0010 100644 --- a/docs/ACCESS.md +++ b/docs/ACCESS.md @@ -20,21 +20,47 @@ --- -## 2. SSH Deployment Key +## 2. SSH Keys -Der Deployment Key liegt im Repository unter `.keys/`: +Alle Keys liegen im Repository unter `.keys/` (Repo ist nur intern verfuegbar). -| Datei | Beschreibung | -|------------------------------|-----------------------| -| `.keys/deploy_ed25519` | Private Key (Ed25519) | -| `.keys/deploy_ed25519.pub` | Public Key | +### 2.1 Deploy Key (Server-Zugriff) -### Public Key (zur Hinterlegung auf Servern) +Fuer den SSH-Zugriff auf den Entwicklungsserver `insight-dev-01`. + +| Datei | Beschreibung | +|------------------------------|---------------------------------| +| `.keys/deploy_ed25519` | Private Key (Server-Zugriff) | +| `.keys/deploy_ed25519.pub` | Public Key | + +**Public Key:** ``` ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMuTpqzLyjqTIDMJ4bwEE4o2JeHH3imL+NeipeuBfiTo insight-deploy@xinion.lan ``` -### SSH-Verbindung zum Server +**Hinterlegen auf:** Server `insight-dev-01` in `/home/deploy/.ssh/authorized_keys` + +### 2.2 CI/CD Key (Forgejo Actions) + +Fuer automatisierte Deployments durch die Forgejo Actions CI/CD-Pipeline. +Die Pipeline nutzt diesen Key, um sich per SSH auf den Server zu verbinden +und Docker-Container zu aktualisieren. + +| Datei | Beschreibung | +|------------------------------|---------------------------------| +| `.keys/cicd_ed25519` | Private Key (CI/CD Pipeline) | +| `.keys/cicd_ed25519.pub` | Public Key | + +**Public Key:** +``` +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINlPo+AvDMTMZC0G49o+kuU98/aC85N90QU3a+FaTjoG insight-cicd@xinion.lan +``` + +**Hinterlegen auf:** +1. Server `insight-dev-01` in `/home/deploy/.ssh/authorized_keys` +2. Forgejo: Repository Settings > Secrets (als `SSH_DEPLOY_KEY` fuer Actions) + +### 2.3 SSH-Verbindung zum Server ```bash # Verbindung zum Entwicklungsserver: ssh -i .keys/deploy_ed25519 deploy@ @@ -48,9 +74,12 @@ Host insight-dev StrictHostKeyChecking accept-new ``` -### Wo der Public Key hinterlegt werden muss -1. **Entwicklungsserver (VM)**: `/home/deploy/.ssh/authorized_keys` -2. **Forgejo**: Repository Settings > Deploy Keys (fuer CI/CD) +### 2.4 Wo welcher Key hinterlegt werden muss + +| Key | Server `authorized_keys` | Forgejo Secrets | +|--------------|--------------------------|------------------------| +| deploy | Ja | Nein | +| cicd | Ja | Ja (`SSH_DEPLOY_KEY`) | --- From b3516c5fdd83c828c4a0d4846accbb202b0c0c59 Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Sun, 8 Mar 2026 10:36:22 +0100 Subject: [PATCH 03/50] docs: add Forgejo CI/CD setup guide for Git server configuration Step-by-step instructions for: - Enabling Forgejo Actions and Container Registry - Installing and registering a Forgejo Runner - Configuring repository secrets for deployment - Setting up branch protection rules - Testing the Container Registry Co-Authored-By: Claude Opus 4.6 --- docs/FORGEJO_SETUP.md | 274 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 docs/FORGEJO_SETUP.md diff --git a/docs/FORGEJO_SETUP.md b/docs/FORGEJO_SETUP.md new file mode 100644 index 0000000..f06663a --- /dev/null +++ b/docs/FORGEJO_SETUP.md @@ -0,0 +1,274 @@ +# Forgejo Setup - Anleitung fuer den Git-Server + +> **Ziel:** Forgejo so konfigurieren, dass CI/CD Pipelines (Forgejo Actions) +> automatisch laufen und Docker Images in die integrierte Container Registry +> gepusht werden. + +--- + +## 1. Voraussetzungen pruefen + +### 1.1 Forgejo Actions aktiviert? + +Forgejo Actions ist seit Forgejo 1.21+ verfuegbar, muss aber in der +Server-Konfiguration aktiviert sein. + +In der Forgejo-Konfiguration (`app.ini` auf dem Git-Server) pruefen: + +```ini +# /etc/forgejo/app.ini (oder wo Forgejo installiert ist) + +[actions] +ENABLED = true +DEFAULT_ACTIONS_URL = https://code.forgejo.org +``` + +Falls nicht vorhanden: Eintrag hinzufuegen und Forgejo neustarten. + +```bash +sudo systemctl restart forgejo +``` + +### 1.2 Container Registry aktiviert? + +Die Registry muss ebenfalls in `app.ini` aktiviert sein: + +```ini +[packages] +ENABLED = true +``` + +--- + +## 2. Forgejo Actions Runner einrichten + +Forgejo Actions braucht einen **Runner** (vergleichbar mit GitHub Actions +Self-Hosted Runner). Der Runner fuehrt die CI/CD-Jobs aus. + +### 2.1 Runner auf dem Git-Server installieren + +```bash +# Runner-Binary herunterladen (aktuelle Version pruefen auf: +# https://code.forgejo.org/forgejo/runner/releases) + +# Beispiel fuer Linux x86_64: +wget https://code.forgejo.org/forgejo/runner/releases/download/v4.0.0/forgejo-runner-4.0.0-linux-amd64 +chmod +x forgejo-runner-4.0.0-linux-amd64 +sudo mv forgejo-runner-4.0.0-linux-amd64 /usr/local/bin/forgejo-runner +``` + +### 2.2 Runner registrieren + +Zuerst ein **Registration Token** in Forgejo generieren: + +1. Forgejo Web-UI oeffnen: `https://git.xinion.lan` +2. **Site Administration** > **Runners** (oben rechts Zahnrad-Icon) +3. Oder: **Repository** > **Settings** > **Actions** > **Runners** +4. Klick auf **"Create new Runner"** oder **"Registration Token"** +5. Token kopieren + +Dann auf dem Server den Runner registrieren: + +```bash +# Runner registrieren (interaktiv) +forgejo-runner register \ + --instance https://git.xinion.lan \ + --token \ + --name insight-runner \ + --labels ubuntu-latest:docker://node:20 + +# Alternativ: non-interaktiv +forgejo-runner register --no-interactive \ + --instance https://git.xinion.lan \ + --token \ + --name insight-runner \ + --labels "ubuntu-latest:docker://node:20,docker:docker://docker:latest" +``` + +### 2.3 Runner als Systemd-Service einrichten + +```bash +# Service-Datei erstellen +sudo tee /etc/systemd/system/forgejo-runner.service > /dev/null << 'UNIT' +[Unit] +Description=Forgejo Actions Runner +After=docker.service + +[Service] +Type=simple +User=forgejo-runner +WorkingDirectory=/opt/forgejo-runner +ExecStart=/usr/local/bin/forgejo-runner daemon +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +UNIT + +# User und Verzeichnis anlegen +sudo useradd -r -s /bin/false forgejo-runner +sudo mkdir -p /opt/forgejo-runner +sudo chown forgejo-runner:forgejo-runner /opt/forgejo-runner + +# Docker-Zugriff fuer den Runner +sudo usermod -aG docker forgejo-runner + +# Service starten +sudo systemctl daemon-reload +sudo systemctl enable forgejo-runner +sudo systemctl start forgejo-runner + +# Status pruefen +sudo systemctl status forgejo-runner +``` + +### 2.4 Pruefen ob Runner aktiv ist + +In Forgejo Web-UI: +- **Site Administration** > **Runners** +- Der Runner `insight-runner` sollte mit Status **"Online"** erscheinen + +--- + +## 3. Repository-Einstellungen + +### 3.1 Actions fuer das Repository aktivieren + +1. Repository oeffnen: `https://git.xinion.lan/gitadmin/INSIGHT-MVP` +2. **Settings** > **Repository** +3. Unter **"Actions"**: Haken setzen bei **"Enable Repository Actions"** +4. Speichern + +### 3.2 Repository Secrets anlegen + +Die CI/CD-Pipeline braucht Secrets fuer den Server-Zugang. + +1. **Settings** > **Actions** > **Secrets** +2. Folgende Secrets anlegen: + +| Secret Name | Wert | Zweck | +|---------------------|---------------------------------------------------------|-----------------------------| +| `SSH_DEPLOY_KEY` | Inhalt von `.keys/cicd_ed25519` (Private Key) | SSH-Zugriff auf Server | +| `DEPLOY_HOST` | IP-Adresse von `insight-dev-01` | Server-Adresse | +| `DEPLOY_USER` | `deploy` | SSH-User auf dem Server | +| `REGISTRY_USER` | Forgejo-Username (z.B. `gitadmin`) | Container Registry Login | +| `REGISTRY_PASSWORD` | Forgejo-Passwort oder Access Token | Container Registry Login | + +#### SSH Key als Secret hinterlegen + +Den **kompletten** Inhalt des Private Keys kopieren: + +```bash +# Auf dem MacBook ausfuehren: +cat .keys/cicd_ed25519 +``` + +Den gesamten Output (inkl. `-----BEGIN OPENSSH PRIVATE KEY-----` und +`-----END OPENSSH PRIVATE KEY-----`) in das Secret-Feld einfuegen. + +#### Forgejo Access Token erstellen (fuer Registry) + +Statt Passwort empfehlen wir einen **Access Token**: + +1. Forgejo Web-UI > **Profil** (oben rechts) > **Settings** > **Applications** +2. **Generate New Token** +3. Name: `insight-registry` +4. Berechtigungen: `write:package` (oder `package` Scope) +5. Token kopieren und als `REGISTRY_PASSWORD` Secret anlegen + +### 3.3 Branch Protection einrichten + +1. **Settings** > **Branches** +2. **Add Branch Protection Rule** + +Fuer `main`: +- **Branch name pattern:** `main` +- **Enable push:** Deaktiviert (nur via Merge) +- **Required approvals:** 1 +- **Status checks must pass:** Aktiviert + +Fuer `develop`: +- **Branch name pattern:** `develop` +- **Enable push:** Deaktiviert (nur via Merge) +- **Required approvals:** 1 +- **Status checks must pass:** Aktiviert + +--- + +## 4. Container Registry testen + +### 4.1 Vom MacBook aus testen + +```bash +# In die Forgejo Registry einloggen +docker login git.xinion.lan -u +# Passwort oder Access Token eingeben + +# Test-Image taggen und pushen +docker pull hello-world +docker tag hello-world git.xinion.lan/gitadmin/insight-test:latest +docker push git.xinion.lan/gitadmin/insight-test:latest + +# Aufraumen +docker rmi git.xinion.lan/gitadmin/insight-test:latest +``` + +### 4.2 In Forgejo pruefen + +1. Repository > **Packages** Tab +2. Das `insight-test` Image sollte sichtbar sein +3. Nach dem Test kann es geloescht werden + +--- + +## 5. Checkliste + +Bitte alle Punkte abarbeiten und abhaken: + +- [ ] `app.ini`: `[actions] ENABLED = true` gesetzt +- [ ] `app.ini`: `[packages] ENABLED = true` gesetzt +- [ ] Forgejo neugestartet nach Config-Aenderung +- [ ] Forgejo Runner installiert und registriert +- [ ] Runner laeuft als Systemd-Service und ist "Online" +- [ ] Repository Actions aktiviert (Settings > Repository) +- [ ] Secret `SSH_DEPLOY_KEY` angelegt (CI/CD Private Key) +- [ ] Secret `DEPLOY_HOST` angelegt (Server-IP) +- [ ] Secret `DEPLOY_USER` angelegt (`deploy`) +- [ ] Secret `REGISTRY_USER` angelegt (Forgejo Username) +- [ ] Secret `REGISTRY_PASSWORD` angelegt (Access Token) +- [ ] Branch Protection fuer `main` eingerichtet +- [ ] Branch Protection fuer `develop` eingerichtet +- [ ] Container Registry getestet (docker push/pull funktioniert) + +--- + +## 6. Haeufige Probleme + +### Runner startet nicht +```bash +# Logs pruefen +sudo journalctl -u forgejo-runner -f + +# Hat der Runner Docker-Zugriff? +sudo -u forgejo-runner docker ps +``` + +### Actions werden nicht getriggert +- Ist Actions im Repository aktiviert? (Settings > Repository) +- Liegt die Workflow-Datei unter `.forgejo/workflows/`? +- Ist der Runner online? (Site Administration > Runners) + +### Registry Push schlaegt fehl +```bash +# Login testen +docker login git.xinion.lan + +# Ist die Registry aktiviert? +# In app.ini: [packages] ENABLED = true +``` + +### Branch Protection blockiert Push +- Korrekt! Main und Develop sind geschuetzt. +- Feature-Branches erstellen: `git checkout -b feature/mein-feature` +- Push auf Feature-Branch, dann Pull Request erstellen From 34129401a323af346f15484a5d01254bc37bead4 Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Sun, 8 Mar 2026 11:36:16 +0100 Subject: [PATCH 04/50] docs: update ACCESS.md with server IPs and Forgejo setup completion - Add insight-dev-01 server IP: 172.20.10.59 - Add Git server (GAIA-GIT) details: 172.20.10.11 - Replace all IP placeholders with actual values - Update Summarize.md with Forgejo configuration status Server setup completed on git.xinion.lan: - Docker Engine 29.3 installed - Forgejo Actions + Container Registry enabled - Runner v6.3.1 registered and running - 5 repository secrets configured - Branch protection on main + develop Co-Authored-By: Claude Opus 4.6 --- Summarize.md | 65 ++++++++++++++++++++++++++++---------------------- docs/ACCESS.md | 29 +++++++++++++++------- 2 files changed, 57 insertions(+), 37 deletions(-) diff --git a/Summarize.md b/Summarize.md index ccab691..da5a218 100644 --- a/Summarize.md +++ b/Summarize.md @@ -8,59 +8,66 @@ ### Aenderungen in dieser Session -#### Projektinitialisierung & Infrastruktur-Definition +#### 1. Projektinitialisierung & Infrastruktur-Definition **Was wurde gemacht:** -1. **SSH Deployment Key erstellt** - - Ed25519-Schluessel unter `.keys/deploy_ed25519` generiert - - Public Key muss auf dem Entwicklungsserver und in Forgejo hinterlegt werden - - Key-Kommentar: `insight-deploy@xinion.lan` +1. **SSH Keys erstellt** + - Deploy-Key (`.keys/deploy_ed25519`) fuer Server-Zugriff + - CI/CD-Key (`.keys/cicd_ed25519`) fuer Forgejo Actions Pipeline 2. **Infrastruktur-Definition erstellt** (`docs/INFRASTRUCTURE.md`) - ProxmoxVE VM-Spezifikation: Ubuntu 24.04 LTS, 4 vCPU, 8 GB RAM, 60 GB SSD - Docker-Netzwerk-Architektur mit 3 isolierten Netzwerken - - Komplette Service-Landschaft definiert (Traefik, Core, Frontend, PostgreSQL, PgBouncer, Redis, step-ca) - - Observability-Stack definiert (Prometheus, Grafana, Loki, Tempo, Promtail, cAdvisor) + - Komplette Service-Landschaft definiert - Schritt-fuer-Schritt VM-Setup Anleitung 3. **Zugangsdaten-Dokument erstellt** (`docs/ACCESS.md`) - - Git Repository Zugangsdaten - - SSH-Key Dokumentation und Verwendung - - Server-Zugangsdaten (Platzhalter fuer IP) - - Alle Service-Ports dokumentiert - - Deployment-Pfad dokumentiert - - Wichtige Befehle (MacBook & Server) + - Server-IP: 172.20.10.59 (insight-dev-01) + - Git-Server: 172.20.10.11 (GAIA-GIT) + - Alle SSH-Keys, Ports, Befehle dokumentiert -4. **Projektstruktur aufgesetzt** - - Verzeichnisstruktur gemaess Briefing angelegt - - packages/core-service/ (NestJS Backend) - - packages/frontend/ (React + Vite) - - config/ (Traefik, Prometheus, step-ca) - - .forgejo/workflows/ (CI/CD) +4. **Projektstruktur aufgesetzt** (packages/core-service, packages/frontend, config, .forgejo) -5. **Basis-Konfigurationsdateien erstellt** - - `.gitignore` - alle relevanten Ausschluesse - - `.env.example` - alle Umgebungsvariablen dokumentiert (ohne Werte) - - `README.md` - vollstaendiges Onboarding-Dokument +5. **Basis-Konfigurationsdateien** (.gitignore, .env.example, README.md) + +#### 2. Forgejo Git-Server Konfiguration + +**Was wurde auf dem Git-Server (172.20.10.11) gemacht:** + +1. **Docker Engine 29.3 installiert** (fuer Forgejo Actions Runner) +2. **Forgejo Actions aktiviert** (`[actions] ENABLED = true` in app.ini) +3. **Container Registry aktiviert** (`[packages] ENABLED = true` in app.ini) +4. **Forgejo Runner v6.3.1 installiert und registriert** + - Runner-Name: `insight-runner` + - Labels: `ubuntu-latest` (docker://node:20) + - Laeuft als Systemd-Service (`forgejo-runner.service`) +5. **Repository Secrets angelegt:** + - `SSH_DEPLOY_KEY` - CI/CD Private Key + - `DEPLOY_HOST` - 172.20.10.59 + - `DEPLOY_USER` - deploy + - `REGISTRY_USER` - gitadmin + - `REGISTRY_PASSWORD` - Forgejo Access Token +6. **Branch Protection eingerichtet:** + - `main`: Kein direkter Push, 1 Approval erforderlich + - `develop`: Kein direkter Push, 1 Approval erforderlich +7. **Forgejo Setup-Anleitung erstellt** (`docs/FORGEJO_SETUP.md`) --- ### Naechste Schritte -- [ ] VM in ProxmoxVE erstellen und konfigurieren -- [ ] SSH Deploy Key auf Server und in Forgejo hinterlegen +- [ ] SSH Deploy Keys auf insight-dev-01 Server hinterlegen - [ ] `docker-compose.yml` erstellen (alle Basis-Services) - [ ] `docker-compose.observability.yml` erstellen - [ ] NestJS Core-Service implementieren (Auth, Users, Tenants) - [ ] Prisma-Schemas erstellen (core + tenant) - [ ] React Frontend-Shell implementieren -- [ ] CI/CD Pipelines in Forgejo Actions definieren +- [ ] CI/CD Pipelines (.forgejo/workflows/) definieren --- ### Offene Fragen / Abhaengigkeiten -- Server-IP wird erst bei VM-Erstellung vergeben -- DNS-Eintraege (insight-dev.xinion.lan) muessen konfiguriert werden -- Forgejo Deploy Key muss manuell hinterlegt werden +- DNS-Eintrag `insight-dev.xinion.lan` muss auf 172.20.10.59 zeigen +- Deploy Keys muessen auf insight-dev-01 in authorized_keys hinterlegt werden diff --git a/docs/ACCESS.md b/docs/ACCESS.md index 6de0010..dfd8b48 100644 --- a/docs/ACCESS.md +++ b/docs/ACCESS.md @@ -63,12 +63,12 @@ ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINlPo+AvDMTMZC0G49o+kuU98/aC85N90QU3a+FaTjoG ### 2.3 SSH-Verbindung zum Server ```bash # Verbindung zum Entwicklungsserver: -ssh -i .keys/deploy_ed25519 deploy@ +ssh -i .keys/deploy_ed25519 deploy@172.20.10.59 # Mit SSH-Config (empfohlen): # Eintrag in ~/.ssh/config: Host insight-dev - HostName + HostName 172.20.10.59 User deploy IdentityFile ~/git.xinion.lan/INSIGHT-MVP/.keys/deploy_ed25519 StrictHostKeyChecking accept-new @@ -89,7 +89,7 @@ Host insight-dev |------------------|-----------------------------------------| | **Hostname** | `insight-dev-01` | | **OS** | Ubuntu 24.04 LTS | -| **IP** | _wird bei VM-Erstellung vergeben_ | +| **IP** | `172.20.10.59` | | **SSH-Port** | 22 | | **SSH-User** | `deploy` | | **SSH-Key** | `.keys/deploy_ed25519` | @@ -99,7 +99,7 @@ Host insight-dev ### Schnellzugriff nach VM-Setup ```bash # SSH auf den Server -ssh -i .keys/deploy_ed25519 deploy@ +ssh -i .keys/deploy_ed25519 deploy@172.20.10.59 # Status aller Container pruefen docker compose ps @@ -134,7 +134,7 @@ docker compose restart core | Service | Port | Zugriff | |-----------------|-------|----------------------------------| -| Grafana | 3001 | SSH-Tunnel: `ssh -L 3001:localhost:3001 deploy@` | +| Grafana | 3001 | SSH-Tunnel: `ssh -L 3001:localhost:3001 deploy@172.20.10.59` | | Prometheus | 9090 | Nur intern | | Loki | 3100 | Nur intern | | Tempo | 3200 | Nur intern | @@ -197,7 +197,20 @@ Laufende Anwendung --- -## 8. Wichtige Befehle +## 8. Git-Server (Forgejo) + +| Parameter | Wert | +|------------------|-----------------------------------------| +| **Hostname** | `git.xinion.lan` | +| **IP** | `172.20.10.11` | +| **SSH-User** | `sysadmin` | +| **SSH-Port** | 22 | +| **Web-UI** | `https://git.xinion.lan` | +| **Forgejo-User** | `gitadmin` | + +--- + +## 9. Wichtige Befehle ### Vom MacBook aus ```bash @@ -205,10 +218,10 @@ Laufende Anwendung git push origin develop # SSH auf Server -ssh -i .keys/deploy_ed25519 deploy@ +ssh -i .keys/deploy_ed25519 deploy@172.20.10.59 # Grafana oeffnen (SSH-Tunnel) -ssh -L 3001:localhost:3001 -i .keys/deploy_ed25519 deploy@ +ssh -L 3001:localhost:3001 -i .keys/deploy_ed25519 deploy@172.20.10.59 # Dann im Browser: http://localhost:3001 ``` From 10f291cdda5a19ac4b9803e79e1e9a3ffb696ac1 Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Sun, 8 Mar 2026 15:33:36 +0100 Subject: [PATCH 05/50] feat: implement Sprint 1 Alpha - full stack with Docker, NestJS, React Docker Infrastructure: - docker-compose.yml with Traefik 3, PostgreSQL 16, PgBouncer, Redis 7, step-ca - docker-compose.observability.yml with Prometheus, Grafana, Loki, Tempo, Promtail - Traefik dynamic config (TLS, security headers, CORS, compression) - PostgreSQL init script (uuid-ossp, pgcrypto, pg_trgm extensions) - Grafana auto-provisioned datasources (Prometheus, Loki, Tempo) NestJS Core-Service: - Auth module: Login (email/password), TOTP 2FA, JWT RS256, token refresh/revocation - Users module: CRUD, bcrypt cost 12, pagination, role-based access - Tenants module: CRUD, member management, slug validation - Prisma schemas: core (Users, AuthProviders, Tenants, Modules, AuditLog) tenant (Contacts, Activities - CRM reference for Sprint 2) - TenantPrismaService: Dynamic per-tenant DB connections with caching - RedisService: Token blocklist, refresh token families, generic cache - Global JwtAuthGuard with @Public() decorator, RolesGuard, GlobalExceptionFilter - Health endpoint with DB + Redis status checks - Swagger API documentation (dev only) - Multi-stage Dockerfile (dev + production) React Frontend: - Vite 6 + React 18 + TypeScript strict - AuthContext with silent refresh (access token in memory, NOT localStorage) - Login page with TOTP 2FA support - App shell with sidebar navigation - Admin pages: Users + Tenants management tables - API client with automatic token refresh interceptor - Multi-stage Dockerfile (dev + nginx production) CI/CD Pipelines: - ci.yml: Lint, type-check, test, build on all branches - deploy.yml: Docker build, push to Forgejo registry, SSH deploy Co-Authored-By: Claude Opus 4.6 --- .forgejo/workflows/ci.yml | 82 +++++ .forgejo/workflows/deploy.yml | 108 ++++++ Summarize.md | 179 +++++++++- .../provisioning/datasources/datasources.yml | 47 +++ config/loki/loki.yml | 37 +++ config/postgres/init/01-init-extensions.sql | 22 ++ config/prometheus/.gitkeep | 0 config/prometheus/prometheus.yml | 40 +++ config/promtail/promtail.yml | 51 +++ config/tempo/tempo.yml | 41 +++ config/traefik/.gitkeep | 0 config/traefik/dynamic/middlewares.yml | 52 +++ config/traefik/dynamic/tls.yml | 24 ++ docker-compose.observability.yml | 185 +++++++++++ docker-compose.yml | 307 ++++++++++++++++++ packages/core-service/.dockerignore | 7 + packages/core-service/Dockerfile | 56 ++++ packages/core-service/nest-cli.json | 8 + packages/core-service/package.json | 93 ++++++ packages/core-service/prisma/.gitkeep | 0 .../core-service/prisma/core.schema.prisma | 181 +++++++++++ .../core-service/prisma/tenant.schema.prisma | 111 +++++++ packages/core-service/src/app.module.ts | 57 ++++ .../src/common/decorators/.gitkeep | 0 .../decorators/current-user.decorator.ts | 35 ++ .../src/common/decorators/index.ts | 3 + .../src/common/decorators/public.decorator.ts | 16 + .../src/common/decorators/roles.decorator.ts | 13 + .../core-service/src/common/filters/.gitkeep | 0 .../common/filters/global-exception.filter.ts | 88 +++++ .../core-service/src/common/guards/.gitkeep | 0 .../src/common/guards/jwt-auth.guard.ts | 65 ++++ .../src/common/guards/roles.guard.ts | 37 +++ .../src/common/interceptors/.gitkeep | 0 packages/core-service/src/config/.gitkeep | 0 .../core-service/src/config/env.validation.ts | 98 ++++++ packages/core-service/src/core/auth/.gitkeep | 0 .../src/core/auth/auth.controller.ts | 121 +++++++ .../core-service/src/core/auth/auth.module.ts | 47 +++ .../src/core/auth/auth.service.ts | 264 +++++++++++++++ .../src/core/auth/dto/login.dto.ts | 30 ++ .../src/core/auth/strategies/jwt.strategy.ts | 47 +++ .../src/core/auth/totp.service.ts | 54 +++ .../core-service/src/core/modules/.gitkeep | 0 .../core-service/src/core/tenants/.gitkeep | 0 .../src/core/tenants/dto/add-member.dto.ts | 19 ++ .../src/core/tenants/dto/create-tenant.dto.ts | 37 +++ .../src/core/tenants/dto/update-tenant.dto.ts | 29 ++ .../src/core/tenants/tenants.controller.ts | 110 +++++++ .../src/core/tenants/tenants.module.ts | 10 + .../src/core/tenants/tenants.service.ts | 166 ++++++++++ packages/core-service/src/core/users/.gitkeep | 0 .../src/core/users/dto/create-user.dto.ts | 46 +++ .../src/core/users/dto/update-user.dto.ts | 21 ++ .../src/core/users/users.controller.ts | 89 +++++ .../src/core/users/users.module.ts | 10 + .../src/core/users/users.service.ts | 174 ++++++++++ .../src/health/health.controller.ts | 72 ++++ .../core-service/src/health/health.module.ts | 10 + packages/core-service/src/main.ts | 82 +++++ packages/core-service/src/prisma/.gitkeep | 0 .../core-service/src/prisma/prisma.module.ts | 10 + .../core-service/src/prisma/prisma.service.ts | 32 ++ .../src/prisma/tenant-prisma.service.ts | 103 ++++++ .../core-service/src/redis/redis.module.ts | 9 + .../core-service/src/redis/redis.service.ts | 144 ++++++++ packages/core-service/tsconfig.build.json | 4 + packages/core-service/tsconfig.json | 29 ++ packages/frontend/.dockerignore | 6 + packages/frontend/Dockerfile | 34 ++ packages/frontend/index.html | 13 + packages/frontend/nginx.conf | 33 ++ packages/frontend/package.json | 34 ++ packages/frontend/src/admin/.gitkeep | 0 .../frontend/src/admin/AdminTenantsPage.tsx | 96 ++++++ .../frontend/src/admin/AdminUsersPage.tsx | 106 ++++++ packages/frontend/src/api/client.ts | 76 +++++ packages/frontend/src/auth/.gitkeep | 0 packages/frontend/src/auth/AuthContext.tsx | 130 ++++++++ .../frontend/src/auth/LoginPage.module.css | 106 ++++++ packages/frontend/src/auth/LoginPage.tsx | 113 +++++++ .../src/components/HelpPanel/.gitkeep | 0 .../src/components/HelpTooltip/.gitkeep | 0 packages/frontend/src/index.css | 75 +++++ packages/frontend/src/main.tsx | 29 ++ packages/frontend/src/shell/.gitkeep | 0 packages/frontend/src/shell/App.tsx | 51 +++ .../frontend/src/shell/AppLayout.module.css | 105 ++++++ packages/frontend/src/shell/AppLayout.tsx | 74 +++++ packages/frontend/src/shell/DashboardPage.tsx | 31 ++ packages/frontend/tsconfig.json | 29 ++ packages/frontend/vite-env.d.ts | 1 + packages/frontend/vite.config.ts | 26 ++ 93 files changed, 4972 insertions(+), 8 deletions(-) create mode 100644 .forgejo/workflows/ci.yml create mode 100644 .forgejo/workflows/deploy.yml create mode 100644 config/grafana/provisioning/datasources/datasources.yml create mode 100644 config/loki/loki.yml create mode 100644 config/postgres/init/01-init-extensions.sql delete mode 100644 config/prometheus/.gitkeep create mode 100644 config/prometheus/prometheus.yml create mode 100644 config/promtail/promtail.yml create mode 100644 config/tempo/tempo.yml delete mode 100644 config/traefik/.gitkeep create mode 100644 config/traefik/dynamic/middlewares.yml create mode 100644 config/traefik/dynamic/tls.yml create mode 100644 docker-compose.observability.yml create mode 100644 docker-compose.yml create mode 100644 packages/core-service/.dockerignore create mode 100644 packages/core-service/Dockerfile create mode 100644 packages/core-service/nest-cli.json create mode 100644 packages/core-service/package.json delete mode 100644 packages/core-service/prisma/.gitkeep create mode 100644 packages/core-service/prisma/core.schema.prisma create mode 100644 packages/core-service/prisma/tenant.schema.prisma create mode 100644 packages/core-service/src/app.module.ts delete mode 100644 packages/core-service/src/common/decorators/.gitkeep create mode 100644 packages/core-service/src/common/decorators/current-user.decorator.ts create mode 100644 packages/core-service/src/common/decorators/index.ts create mode 100644 packages/core-service/src/common/decorators/public.decorator.ts create mode 100644 packages/core-service/src/common/decorators/roles.decorator.ts delete mode 100644 packages/core-service/src/common/filters/.gitkeep create mode 100644 packages/core-service/src/common/filters/global-exception.filter.ts delete mode 100644 packages/core-service/src/common/guards/.gitkeep create mode 100644 packages/core-service/src/common/guards/jwt-auth.guard.ts create mode 100644 packages/core-service/src/common/guards/roles.guard.ts delete mode 100644 packages/core-service/src/common/interceptors/.gitkeep delete mode 100644 packages/core-service/src/config/.gitkeep create mode 100644 packages/core-service/src/config/env.validation.ts delete mode 100644 packages/core-service/src/core/auth/.gitkeep create mode 100644 packages/core-service/src/core/auth/auth.controller.ts create mode 100644 packages/core-service/src/core/auth/auth.module.ts create mode 100644 packages/core-service/src/core/auth/auth.service.ts create mode 100644 packages/core-service/src/core/auth/dto/login.dto.ts create mode 100644 packages/core-service/src/core/auth/strategies/jwt.strategy.ts create mode 100644 packages/core-service/src/core/auth/totp.service.ts delete mode 100644 packages/core-service/src/core/modules/.gitkeep delete mode 100644 packages/core-service/src/core/tenants/.gitkeep create mode 100644 packages/core-service/src/core/tenants/dto/add-member.dto.ts create mode 100644 packages/core-service/src/core/tenants/dto/create-tenant.dto.ts create mode 100644 packages/core-service/src/core/tenants/dto/update-tenant.dto.ts create mode 100644 packages/core-service/src/core/tenants/tenants.controller.ts create mode 100644 packages/core-service/src/core/tenants/tenants.module.ts create mode 100644 packages/core-service/src/core/tenants/tenants.service.ts delete mode 100644 packages/core-service/src/core/users/.gitkeep create mode 100644 packages/core-service/src/core/users/dto/create-user.dto.ts create mode 100644 packages/core-service/src/core/users/dto/update-user.dto.ts create mode 100644 packages/core-service/src/core/users/users.controller.ts create mode 100644 packages/core-service/src/core/users/users.module.ts create mode 100644 packages/core-service/src/core/users/users.service.ts create mode 100644 packages/core-service/src/health/health.controller.ts create mode 100644 packages/core-service/src/health/health.module.ts create mode 100644 packages/core-service/src/main.ts delete mode 100644 packages/core-service/src/prisma/.gitkeep create mode 100644 packages/core-service/src/prisma/prisma.module.ts create mode 100644 packages/core-service/src/prisma/prisma.service.ts create mode 100644 packages/core-service/src/prisma/tenant-prisma.service.ts create mode 100644 packages/core-service/src/redis/redis.module.ts create mode 100644 packages/core-service/src/redis/redis.service.ts create mode 100644 packages/core-service/tsconfig.build.json create mode 100644 packages/core-service/tsconfig.json create mode 100644 packages/frontend/.dockerignore create mode 100644 packages/frontend/Dockerfile create mode 100644 packages/frontend/index.html create mode 100644 packages/frontend/nginx.conf create mode 100644 packages/frontend/package.json delete mode 100644 packages/frontend/src/admin/.gitkeep create mode 100644 packages/frontend/src/admin/AdminTenantsPage.tsx create mode 100644 packages/frontend/src/admin/AdminUsersPage.tsx create mode 100644 packages/frontend/src/api/client.ts delete mode 100644 packages/frontend/src/auth/.gitkeep create mode 100644 packages/frontend/src/auth/AuthContext.tsx create mode 100644 packages/frontend/src/auth/LoginPage.module.css create mode 100644 packages/frontend/src/auth/LoginPage.tsx delete mode 100644 packages/frontend/src/components/HelpPanel/.gitkeep delete mode 100644 packages/frontend/src/components/HelpTooltip/.gitkeep create mode 100644 packages/frontend/src/index.css create mode 100644 packages/frontend/src/main.tsx delete mode 100644 packages/frontend/src/shell/.gitkeep create mode 100644 packages/frontend/src/shell/App.tsx create mode 100644 packages/frontend/src/shell/AppLayout.module.css create mode 100644 packages/frontend/src/shell/AppLayout.tsx create mode 100644 packages/frontend/src/shell/DashboardPage.tsx create mode 100644 packages/frontend/tsconfig.json create mode 100644 packages/frontend/vite-env.d.ts create mode 100644 packages/frontend/vite.config.ts diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml new file mode 100644 index 0000000..7247cfa --- /dev/null +++ b/.forgejo/workflows/ci.yml @@ -0,0 +1,82 @@ +# ============================================================ +# INSIGHT MVP - CI Pipeline (Lint, Type-Check, Test, Build) +# ============================================================ +# Wird bei jedem Push und Pull Request ausgefuehrt. +# ============================================================ + +name: CI + +on: + push: + branches: [main, develop, 'feature/**', 'fix/**', 'hotfix/**'] + pull_request: + branches: [main, develop] + +jobs: + # -------------------------------------------------------- + # Core-Service: Lint, Type-Check, Test, Build + # -------------------------------------------------------- + core-service: + name: Core-Service CI + runs-on: ubuntu-latest + defaults: + run: + working-directory: packages/core-service + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm ci + + - name: Generate Prisma Client + run: npx prisma generate --schema=prisma/core.schema.prisma + + - name: Lint + run: npm run lint:check + + - name: Type-Check + run: npm run typecheck + + - name: Test + run: npm test -- --passWithNoTests + + - name: Build + run: npm run build + + # -------------------------------------------------------- + # Frontend: Lint, Type-Check, Build + # -------------------------------------------------------- + frontend: + name: Frontend CI + runs-on: ubuntu-latest + defaults: + run: + working-directory: packages/frontend + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint:check + + - name: Type-Check + run: npm run typecheck + + - name: Build + run: npm run build diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml new file mode 100644 index 0000000..8315c46 --- /dev/null +++ b/.forgejo/workflows/deploy.yml @@ -0,0 +1,108 @@ +# ============================================================ +# INSIGHT MVP - Deploy Pipeline +# ============================================================ +# Baut Docker-Images, pusht sie in die Forgejo Registry +# und deployed auf den insight-dev-01 Server. +# +# Wird nur bei Push auf 'main' oder 'develop' ausgefuehrt. +# ============================================================ + +name: Deploy + +on: + push: + branches: [main, develop] + +jobs: + # -------------------------------------------------------- + # Docker Images bauen und in Registry pushen + # -------------------------------------------------------- + build-and-push: + name: Build & Push Images + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Determine Tag + id: tag + run: | + if [ "${{ github.ref_name }}" = "main" ]; then + echo "tag=latest" >> $GITHUB_OUTPUT + else + echo "tag=develop" >> $GITHUB_OUTPUT + fi + + - name: Login to Container Registry + run: | + echo "${{ secrets.REGISTRY_PASSWORD }}" | \ + docker login git.xinion.lan -u ${{ secrets.REGISTRY_USER }} --password-stdin + + # Core-Service Image + - name: Build Core-Service + run: | + docker build \ + -t git.xinion.lan/gitadmin/insight-core:${{ steps.tag.outputs.tag }} \ + -f packages/core-service/Dockerfile \ + --target production \ + packages/core-service + + - name: Push Core-Service + run: docker push git.xinion.lan/gitadmin/insight-core:${{ steps.tag.outputs.tag }} + + # Frontend Image + - name: Build Frontend + run: | + docker build \ + -t git.xinion.lan/gitadmin/insight-frontend:${{ steps.tag.outputs.tag }} \ + -f packages/frontend/Dockerfile \ + --target production \ + packages/frontend + + - name: Push Frontend + run: docker push git.xinion.lan/gitadmin/insight-frontend:${{ steps.tag.outputs.tag }} + + # -------------------------------------------------------- + # Auf Server deployen + # -------------------------------------------------------- + deploy: + name: Deploy to Server + runs-on: ubuntu-latest + needs: build-and-push + + steps: + - name: Deploy via SSH + run: | + # SSH-Key vorbereiten + mkdir -p ~/.ssh + echo "${{ secrets.SSH_DEPLOY_KEY }}" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts + + # Deploy-Befehle auf dem Server ausfuehren + ssh -i ~/.ssh/deploy_key ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} << 'DEPLOY' + cd ~/insight + + # Registry Login + echo "${{ secrets.REGISTRY_PASSWORD }}" | \ + docker login git.xinion.lan -u ${{ secrets.REGISTRY_USER }} --password-stdin + + # Neue Images pullen + docker compose pull core frontend + + # Services mit neuem Image starten + docker compose up -d core frontend + + # Health-Check warten + sleep 10 + curl -f http://localhost:3000/health || echo "WARNUNG: Health-Check fehlgeschlagen" + + # Alte Images aufraeumen + docker image prune -f + DEPLOY + + - name: Verify Deployment + run: | + ssh -i ~/.ssh/deploy_key ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} \ + "docker compose ps && echo '--- Deployment erfolgreich ---'" diff --git a/Summarize.md b/Summarize.md index da5a218..4ebafca 100644 --- a/Summarize.md +++ b/Summarize.md @@ -53,21 +53,184 @@ - `develop`: Kein direkter Push, 1 Approval erforderlich 7. **Forgejo Setup-Anleitung erstellt** (`docs/FORGEJO_SETUP.md`) +#### 3. Server-Setup (insight-dev-01) + +**Was wurde auf dem Entwicklungsserver (172.20.10.59) gemacht:** + +1. **SSH Public Keys hinterlegt** in `/home/deploy/.ssh/authorized_keys` + - Deploy-Key (`insight-deploy@xinion.lan`) - fuer manuellen Zugriff + - CI/CD-Key (`insight-cicd@xinion.lan`) - fuer Forgejo Actions Pipeline +2. **SSH-Zugang getestet** - Key-basierter Login als `deploy` funktioniert + +#### 4. Docker Compose & Service-Konfiguration + +**Erstellte Dateien:** + +1. **`docker-compose.yml`** - Alle Basis-Services: + - Traefik 3 (API Gateway, Reverse Proxy, TLS, Rate Limiting) + - PostgreSQL 16-alpine (mit Performance-Tuning fuer 8GB RAM) + - PgBouncer (Connection Pooling, Transaction Mode) + - Redis 7-alpine (Cache, Sessions, Token-Revocation) + - step-ca (Interne Certificate Authority fuer mTLS) + - Core-Service (NestJS) mit Traefik-Labels + - Frontend (React) mit Traefik-Labels + - 3 isolierte Docker-Netzwerke (insight-web, insight-db, insight-cache) + - Health-Checks fuer alle Services + +2. **`docker-compose.observability.yml`** - Monitoring-Stack: + - Prometheus (Metrics-Sammlung, 30 Tage Retention) + - Grafana (Dashboards, automatisch provisionierte Datenquellen) + - Loki (Log-Aggregation) + - Promtail (Docker Log-Collector) + - Tempo (Distributed Tracing, OTLP gRPC) + - cAdvisor (Container-Metriken) + - PostgreSQL Exporter (DB-Metriken) + +3. **Konfigurationsdateien:** + - `config/traefik/dynamic/tls.yml` - TLS-Konfiguration + - `config/traefik/dynamic/middlewares.yml` - Security-Headers, CORS, Compression + - `config/prometheus/prometheus.yml` - Scrape-Konfiguration + - `config/loki/loki.yml` - Log-Storage-Konfiguration + - `config/promtail/promtail.yml` - Docker-Log-Collector + - `config/tempo/tempo.yml` - Tracing-Backend + - `config/grafana/provisioning/datasources/datasources.yml` - Auto-Provisioning + - `config/postgres/init/01-init-extensions.sql` - DB-Extensions (uuid-ossp, pgcrypto, pg_trgm) + +#### 5. NestJS Core-Service Implementierung + +**Projekt-Setup:** +- `package.json` mit allen Dependencies (NestJS 10, Prisma 6, Passport, JWT, bcrypt, TOTP) +- `tsconfig.json` mit strict: true, noImplicitAny, strictNullChecks +- `Dockerfile` (Multi-Stage: base, deps, development, build, production) +- `nest-cli.json` Konfiguration + +**Implementierte Module:** + +1. **Auth-Modul** (`src/core/auth/`) + - `AuthService`: Login (E-Mail/Passwort), Token-Refresh, Logout, Token-Revocation + - `AuthController`: POST /login, /refresh, /logout + - `JwtStrategy`: RS256 Passport-Strategy + - `TotpService`: TOTP 2FA (Google Authenticator kompatibel) + - `LoginDto`: Validierung mit class-validator + - Account-Lockout nach 5 Fehlversuchen (15 Min Sperre) + - Refresh-Token als HttpOnly/Secure/SameSite=Strict Cookie + - Token-Rotation mit Redis-basierter Familien-Erkennung + +2. **Users-Modul** (`src/core/users/`) + - `UsersService`: CRUD, Bcrypt Cost 12, Passwort-Hashing + - `UsersController`: GET /me, GET /users, POST /users, PATCH /users/:id + - DTOs: CreateUserDto, UpdateUserDto + - Paginierung mit Meta-Informationen + +3. **Tenants-Modul** (`src/core/tenants/`) + - `TenantsService`: CRUD, Member-Management + - `TenantsController`: CRUD + POST /:id/members, DELETE /:id/members/:userId + - DTOs: CreateTenantDto, UpdateTenantDto, AddMemberDto + - Slug-Validierung (URL-freundlich) + +4. **Infrastruktur-Module:** + - `PrismaService`: PostgreSQL-Verbindung (platform_core) + - `TenantPrismaService`: Dynamische Tenant-DB-Verbindungen mit Caching + - `RedisService`: Token-Blocklist, Refresh-Token-Familien, generischer Cache + - `HealthController`: GET /health (DB + Redis Status) + +5. **Common (Guards, Decorators, Filter):** + - `@Public()` Decorator fuer oeffentliche Routen + - `@Roles()` Decorator fuer rollenbasierte Zugriffskontrolle + - `@CurrentUser()` Decorator fuer User-Extraktion aus JWT + - `JwtAuthGuard` (global) mit Token-Revocation-Check + - `RolesGuard` fuer Rollen-Pruefung + - `GlobalExceptionFilter` fuer strukturierte Fehlerantworten + +6. **Config:** + - `validateConfig()` mit class-validator fuer Umgebungsvariablen + +#### 6. Prisma-Schemas + +1. **`core.schema.prisma`** (platform_core Datenbank): + - `User` - Plattform-Benutzer (mit Login-Tracking, 2FA) + - `AuthProvider` - Multi-Provider Auth (LOCAL, MS_SSO, M2M) + - `Tenant` - Mandanten mit JSON-Settings + - `TenantMembership` - User-Tenant-Zuordnung (M:N) + - `Module` - Verfuegbare Plattform-Module + - `TenantModule` - Module pro Tenant + - `AuditLog` - Plattform-weites Audit-Log + +2. **`tenant.schema.prisma`** (tenant_{slug} Datenbanken): + - `Contact` - CRM-Kontakte (Person/Organisation) + - `Activity` - CRM-Aktivitaeten (Notiz, Anruf, E-Mail, Meeting, Task) + - Referenz-Schema fuer Sprint 2 (CRM-Modul) + +#### 7. React Frontend-Shell + +**Projekt-Setup:** +- `package.json` mit React 18, Vite 6, React Router 6, TanStack Query 5, Axios +- `tsconfig.json` mit strict TypeScript +- `vite.config.ts` mit API-Proxy und Path-Aliases +- `Dockerfile` (Multi-Stage: development mit Vite, production mit Nginx) +- `nginx.conf` (SPA-Routing, Security-Headers, Caching) + +**Implementierte Komponenten:** + +1. **Auth-System** (`src/auth/`) + - `AuthContext` + `useAuth()` Hook: Login, Logout, Silent Refresh + - `LoginPage`: E-Mail/Passwort + optionaler TOTP 2FA-Code + - Access-Token NUR im Memory (kein localStorage!) + - Automatischer Silent Refresh via HttpOnly Cookie + +2. **API-Client** (`src/api/client.ts`) + - Axios-Instanz mit automatischem Token-Handling + - Request-Interceptor fuer Authorization-Header + - Response-Interceptor fuer automatisches Token-Refresh bei 401 + +3. **App-Shell** (`src/shell/`) + - `App`: React Router mit PrivateRoute-Guard + - `AppLayout`: Sidebar-Navigation + Outlet + - `DashboardPage`: Willkommens-Seite + +4. **Admin-Bereich** (`src/admin/`) + - `AdminUsersPage`: Benutzer-Tabelle mit Paginierung + - `AdminTenantsPage`: Mandanten-Tabelle mit Member-Count + +5. **Styling:** + - CSS Custom Properties (Farben, Layout, Schatten, Radien) + - CSS Modules fuer komponentenspezifische Styles + - Responsive Sidebar-Layout + +#### 8. CI/CD Pipelines + +1. **`.forgejo/workflows/ci.yml`** - Continuous Integration: + - Trigger: Push auf alle Branches + Pull Requests + - Core-Service: npm ci, Prisma Generate, Lint, Type-Check, Test, Build + - Frontend: npm ci, Lint, Type-Check, Build + +2. **`.forgejo/workflows/deploy.yml`** - Deployment: + - Trigger: Push auf main/develop + - Build Docker-Images (Core + Frontend) + - Push in Forgejo Container Registry + - SSH-Deploy auf insight-dev-01 + - Health-Check Verifizierung + --- ### Naechste Schritte -- [ ] SSH Deploy Keys auf insight-dev-01 Server hinterlegen -- [ ] `docker-compose.yml` erstellen (alle Basis-Services) -- [ ] `docker-compose.observability.yml` erstellen -- [ ] NestJS Core-Service implementieren (Auth, Users, Tenants) -- [ ] Prisma-Schemas erstellen (core + tenant) -- [ ] React Frontend-Shell implementieren -- [ ] CI/CD Pipelines (.forgejo/workflows/) definieren +- [x] SSH Deploy Keys auf insight-dev-01 Server hinterlegen +- [x] `docker-compose.yml` erstellen (alle Basis-Services) +- [x] `docker-compose.observability.yml` erstellen +- [x] NestJS Core-Service implementieren (Auth, Users, Tenants) +- [x] Prisma-Schemas erstellen (core + tenant) +- [x] React Frontend-Shell implementieren +- [x] CI/CD Pipelines (.forgejo/workflows/) definieren +- [ ] Docker + Docker Compose auf insight-dev-01 installieren +- [ ] .env-Datei auf Server anlegen (echte Passwoerter) +- [ ] JWT RS256 Schluessel generieren (fuer Token-Signierung) +- [ ] Erste Prisma-Migration ausfuehren +- [ ] Platform-Admin User anlegen (Seed) +- [ ] Erster End-to-End Test (Login -> Dashboard) --- ### Offene Fragen / Abhaengigkeiten - DNS-Eintrag `insight-dev.xinion.lan` muss auf 172.20.10.59 zeigen -- Deploy Keys muessen auf insight-dev-01 in authorized_keys hinterlegt werden diff --git a/config/grafana/provisioning/datasources/datasources.yml b/config/grafana/provisioning/datasources/datasources.yml new file mode 100644 index 0000000..3581867 --- /dev/null +++ b/config/grafana/provisioning/datasources/datasources.yml @@ -0,0 +1,47 @@ +# ============================================================ +# Grafana - Datenquellen (automatisch provisioniert) +# ============================================================ + +apiVersion: 1 + +datasources: + # Prometheus - Metriken + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true + editable: false + + # Loki - Logs + - name: Loki + type: loki + access: proxy + url: http://loki:3100 + editable: false + jsonData: + derivedFields: + - datasourceUid: tempo + matcherRegex: "traceId=(\\w+)" + name: TraceID + url: "$${__value.raw}" + + # Tempo - Traces + - name: Tempo + type: tempo + access: proxy + uid: tempo + url: http://tempo:3200 + editable: false + jsonData: + tracesToLogs: + datasourceUid: loki + tags: ['service'] + mappedTags: [{ key: 'service.name', value: 'service' }] + mapTagNamesEnabled: true + filterByTraceID: true + tracesToMetrics: + datasourceUid: prometheus + tags: [{ key: 'service.name', value: 'service' }] + serviceMap: + datasourceUid: prometheus diff --git a/config/loki/loki.yml b/config/loki/loki.yml new file mode 100644 index 0000000..2d75c2f --- /dev/null +++ b/config/loki/loki.yml @@ -0,0 +1,37 @@ +# ============================================================ +# Loki - Log-Aggregation Konfiguration +# ============================================================ + +auth_enabled: false + +server: + http_listen_port: 3100 + +common: + path_prefix: /loki + storage: + filesystem: + chunks_directory: /loki/chunks + rules_directory: /loki/rules + replication_factor: 1 + ring: + kvstore: + store: inmemory + +schema_config: + configs: + - from: 2024-01-01 + store: tsdb + object_store: filesystem + schema: v13 + index: + prefix: index_ + period: 24h + +limits_config: + retention_period: 30d + reject_old_samples: true + reject_old_samples_max_age: 168h + +analytics: + reporting_enabled: false diff --git a/config/postgres/init/01-init-extensions.sql b/config/postgres/init/01-init-extensions.sql new file mode 100644 index 0000000..2c85932 --- /dev/null +++ b/config/postgres/init/01-init-extensions.sql @@ -0,0 +1,22 @@ +-- ============================================================ +-- PostgreSQL Initialisierung +-- ============================================================ +-- Wird automatisch beim ersten Start ausgefuehrt. +-- Erstellt benoetigte Extensions fuer die platform_core DB. +-- ============================================================ + +-- UUID-Generierung (v4) +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- Kryptographische Funktionen (fuer Token-Hashing etc.) +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; + +-- Trigram-Index fuer Volltextsuche +CREATE EXTENSION IF NOT EXISTS "pg_trgm"; + +-- Bestaetigungsmeldung +DO $$ +BEGIN + RAISE NOTICE 'INSIGHT: PostgreSQL Extensions erfolgreich installiert.'; +END +$$; diff --git a/config/prometheus/.gitkeep b/config/prometheus/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/config/prometheus/prometheus.yml b/config/prometheus/prometheus.yml new file mode 100644 index 0000000..edb798b --- /dev/null +++ b/config/prometheus/prometheus.yml @@ -0,0 +1,40 @@ +# ============================================================ +# Prometheus - Konfiguration +# ============================================================ + +global: + scrape_interval: 15s + evaluation_interval: 15s + scrape_timeout: 10s + +scrape_configs: + # Traefik Metriken + - job_name: "traefik" + static_configs: + - targets: ["traefik:8082"] + + # Core-Service Metriken (NestJS) + - job_name: "core-service" + metrics_path: /metrics + static_configs: + - targets: ["core:3000"] + + # PostgreSQL Exporter + - job_name: "postgres" + static_configs: + - targets: ["postgres-exporter:9187"] + + # cAdvisor (Container-Metriken) + - job_name: "cadvisor" + static_configs: + - targets: ["cadvisor:8080"] + + # Redis (wenn Redis Exporter hinzugefuegt wird) + # - job_name: "redis" + # static_configs: + # - targets: ["redis-exporter:9121"] + + # Prometheus Self-Monitoring + - job_name: "prometheus" + static_configs: + - targets: ["localhost:9090"] diff --git a/config/promtail/promtail.yml b/config/promtail/promtail.yml new file mode 100644 index 0000000..b55aeec --- /dev/null +++ b/config/promtail/promtail.yml @@ -0,0 +1,51 @@ +# ============================================================ +# Promtail - Log-Collector Konfiguration +# ============================================================ +# Sammelt Docker Container Logs und sendet sie an Loki. +# ============================================================ + +server: + http_listen_port: 9080 + grpc_listen_port: 0 + +positions: + filename: /tmp/positions.yaml + +clients: + - url: http://loki:3100/loki/api/v1/push + +scrape_configs: + # Docker Container Logs + - job_name: docker + docker_sd_configs: + - host: unix:///var/run/docker.sock + refresh_interval: 5s + filters: + - name: label + values: ["com.docker.compose.project=insight"] + relabel_configs: + # Container-Name als Label + - source_labels: ['__meta_docker_container_name'] + regex: '/(.*)' + target_label: container + # Compose-Service-Name als Label + - source_labels: ['__meta_docker_container_label_com_docker_compose_service'] + target_label: service + # Log-Stream (stdout/stderr) + - source_labels: ['__meta_docker_container_log_stream'] + target_label: stream + + pipeline_stages: + # JSON-Logs parsen (NestJS) + - json: + expressions: + level: level + message: message + timestamp: timestamp + context: context + - labels: + level: + context: + - timestamp: + source: timestamp + format: RFC3339 diff --git a/config/tempo/tempo.yml b/config/tempo/tempo.yml new file mode 100644 index 0000000..49d695f --- /dev/null +++ b/config/tempo/tempo.yml @@ -0,0 +1,41 @@ +# ============================================================ +# Tempo - Distributed Tracing Konfiguration +# ============================================================ + +server: + http_listen_port: 3200 + +distributor: + receivers: + otlp: + protocols: + grpc: + endpoint: "0.0.0.0:4317" + http: + endpoint: "0.0.0.0:4318" + +storage: + trace: + backend: local + local: + path: /var/tempo/traces + wal: + path: /var/tempo/wal + +metrics_generator: + registry: + external_labels: + source: tempo + cluster: insight-dev + storage: + path: /var/tempo/generator/wal + remote_write: + - url: http://prometheus:9090/api/v1/write + send_exemplars: true + +overrides: + defaults: + metrics_generator: + processors: + - service-graphs + - span-metrics diff --git a/config/traefik/.gitkeep b/config/traefik/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/config/traefik/dynamic/middlewares.yml b/config/traefik/dynamic/middlewares.yml new file mode 100644 index 0000000..9e2c67e --- /dev/null +++ b/config/traefik/dynamic/middlewares.yml @@ -0,0 +1,52 @@ +# ============================================================ +# Traefik - Globale Middlewares +# ============================================================ + +http: + middlewares: + # Security-Headers fuer alle Responses + security-headers: + headers: + browserXssFilter: true + contentTypeNosniff: true + frameDeny: true + stsIncludeSubdomains: true + stsPreload: true + stsSeconds: 31536000 + customFrameOptionsValue: "SAMEORIGIN" + referrerPolicy: "strict-origin-when-cross-origin" + contentSecurityPolicy: >- + default-src 'self'; + script-src 'self' 'unsafe-inline'; + style-src 'self' 'unsafe-inline'; + img-src 'self' data: blob:; + font-src 'self'; + connect-src 'self' wss://insight-dev.xinion.lan; + frame-ancestors 'self'; + + # CORS fuer API + cors-api: + headers: + accessControlAllowMethods: + - GET + - POST + - PUT + - PATCH + - DELETE + - OPTIONS + accessControlAllowHeaders: + - Content-Type + - Authorization + - X-Tenant-ID + - X-Request-ID + accessControlAllowOriginList: + - "https://insight-dev.xinion.lan" + accessControlMaxAge: 86400 + accessControlAllowCredentials: true + addVaryHeader: true + + # Kompression + gzip-compress: + compress: + excludedContentTypes: + - text/event-stream diff --git a/config/traefik/dynamic/tls.yml b/config/traefik/dynamic/tls.yml new file mode 100644 index 0000000..8fed996 --- /dev/null +++ b/config/traefik/dynamic/tls.yml @@ -0,0 +1,24 @@ +# ============================================================ +# Traefik - Dynamische TLS-Konfiguration +# ============================================================ +# Fuer die Alpha-Phase verwenden wir ein selbst-signiertes +# Zertifikat. Spaeter wird step-ca als ACME-Provider genutzt. +# ============================================================ + +tls: + options: + default: + minVersion: VersionTLS12 + cipherSuites: + - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 + - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 + - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 + sniStrict: false + + stores: + default: + defaultGeneratedCert: + resolver: default + domain: + main: "insight-dev.xinion.lan" diff --git a/docker-compose.observability.yml b/docker-compose.observability.yml new file mode 100644 index 0000000..6338a6e --- /dev/null +++ b/docker-compose.observability.yml @@ -0,0 +1,185 @@ +# ============================================================ +# INSIGHT MVP - Docker Compose (Observability-Stack) +# ============================================================ +# Ergaenzt docker-compose.yml um Monitoring, Logging & Tracing. +# +# Nutzung: +# docker compose -f docker-compose.yml -f docker-compose.observability.yml up -d +# +# Grafana (nur via SSH-Tunnel): +# ssh -L 3001:localhost:3001 -i .keys/deploy_ed25519 deploy@172.20.10.59 +# Browser: http://localhost:3001 +# ============================================================ + +networks: + insight-web: + external: true + insight-db: + external: true + +volumes: + prometheus-data: + name: insight-prometheus-data + grafana-data: + name: insight-grafana-data + loki-data: + name: insight-loki-data + tempo-data: + name: insight-tempo-data + +services: + # -------------------------------------------------------- + # Prometheus - Metrics-Sammlung & -Speicherung + # -------------------------------------------------------- + prometheus: + image: prom/prometheus:latest + container_name: insight-prometheus + restart: unless-stopped + command: + - "--config.file=/etc/prometheus/prometheus.yml" + - "--storage.tsdb.path=/prometheus" + - "--storage.tsdb.retention.time=30d" + - "--web.enable-lifecycle" + volumes: + - ./config/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro + - prometheus-data:/prometheus + networks: + - insight-web + ports: + - "127.0.0.1:9090:9090" + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://localhost:9090/-/ready"] + interval: 30s + timeout: 5s + retries: 3 + + # -------------------------------------------------------- + # Grafana - Dashboards & Alerting + # -------------------------------------------------------- + grafana: + image: grafana/grafana:latest + container_name: insight-grafana + restart: unless-stopped + environment: + GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER:-admin} + GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:?GRAFANA_ADMIN_PASSWORD muss gesetzt sein} + GF_SERVER_ROOT_URL: "http://localhost:3001" + GF_SERVER_HTTP_PORT: 3001 + # Datenquellen per Provisioning + GF_PATHS_PROVISIONING: /etc/grafana/provisioning + # Keine anonyme Nutzung + GF_AUTH_ANONYMOUS_ENABLED: "false" + # Logging + GF_LOG_LEVEL: info + volumes: + - grafana-data:/var/lib/grafana + - ./config/grafana/provisioning:/etc/grafana/provisioning:ro + networks: + - insight-web + ports: + - "127.0.0.1:3001:3001" + depends_on: + prometheus: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:3001/api/health || exit 1"] + interval: 30s + timeout: 5s + retries: 3 + + # -------------------------------------------------------- + # Loki - Log-Aggregation + # -------------------------------------------------------- + loki: + image: grafana/loki:latest + container_name: insight-loki + restart: unless-stopped + command: -config.file=/etc/loki/loki.yml + volumes: + - ./config/loki/loki.yml:/etc/loki/loki.yml:ro + - loki-data:/loki + networks: + - insight-web + ports: + - "127.0.0.1:3100:3100" + healthcheck: + test: ["CMD-SHELL", "wget --spider -q http://localhost:3100/ready || exit 1"] + interval: 30s + timeout: 5s + retries: 3 + + # -------------------------------------------------------- + # Promtail - Log-Collector (liest Docker Logs) + # -------------------------------------------------------- + promtail: + image: grafana/promtail:latest + container_name: insight-promtail + restart: unless-stopped + command: -config.file=/etc/promtail/promtail.yml + volumes: + - ./config/promtail/promtail.yml:/etc/promtail/promtail.yml:ro + - /var/log:/var/log:ro + - /var/lib/docker/containers:/var/lib/docker/containers:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + networks: + - insight-web + depends_on: + - loki + + # -------------------------------------------------------- + # Tempo - Distributed Tracing + # -------------------------------------------------------- + tempo: + image: grafana/tempo:latest + container_name: insight-tempo + restart: unless-stopped + command: -config.file=/etc/tempo/tempo.yml + volumes: + - ./config/tempo/tempo.yml:/etc/tempo/tempo.yml:ro + - tempo-data:/var/tempo + networks: + - insight-web + ports: + - "127.0.0.1:3200:3200" # Tempo API + - "127.0.0.1:4317:4317" # OTLP gRPC + healthcheck: + test: ["CMD-SHELL", "wget --spider -q http://localhost:3200/ready || exit 1"] + interval: 30s + timeout: 5s + retries: 3 + + # -------------------------------------------------------- + # cAdvisor - Container-Metriken + # -------------------------------------------------------- + cadvisor: + image: gcr.io/cadvisor/cadvisor:latest + container_name: insight-cadvisor + restart: unless-stopped + privileged: true + volumes: + - /:/rootfs:ro + - /var/run:/var/run:ro + - /sys:/sys:ro + - /var/lib/docker/:/var/lib/docker:ro + - /dev/disk/:/dev/disk:ro + networks: + - insight-web + ports: + - "127.0.0.1:8081:8080" + + # -------------------------------------------------------- + # PostgreSQL Exporter - DB-Metriken fuer Prometheus + # -------------------------------------------------------- + postgres-exporter: + image: prometheuscommunity/postgres-exporter:latest + container_name: insight-postgres-exporter + restart: unless-stopped + environment: + DATA_SOURCE_NAME: "postgresql://${DB_USER:-insight}:${DB_PASSWORD}@postgres:5432/${DB_NAME:-platform_core}?sslmode=disable" + networks: + - insight-web + - insight-db + ports: + - "127.0.0.1:9187:9187" + depends_on: + - postgres diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b092fc2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,307 @@ +# ============================================================ +# INSIGHT MVP - Docker Compose (Basis-Services) +# ============================================================ +# Startet alle Kerndienste der INSIGHT-Plattform. +# Observability-Stack separat: docker-compose.observability.yml +# +# Nutzung: +# docker compose up -d +# docker compose logs -f core +# docker compose ps +# ============================================================ + +networks: + insight-web: + driver: bridge + name: insight-web + insight-db: + driver: bridge + name: insight-db + internal: true + insight-cache: + driver: bridge + name: insight-cache + internal: true + +volumes: + postgres-data: + name: insight-postgres-data + redis-data: + name: insight-redis-data + step-ca-data: + name: insight-step-ca-data + traefik-certs: + name: insight-traefik-certs + +services: + # -------------------------------------------------------- + # Traefik - API Gateway / Reverse Proxy + # -------------------------------------------------------- + traefik: + image: traefik:3 + container_name: insight-traefik + restart: unless-stopped + command: + # API & Dashboard + - "--api.dashboard=true" + - "--api.insecure=true" + # Entrypoints + - "--entrypoints.web.address=:80" + - "--entrypoints.websecure.address=:443" + # HTTP -> HTTPS Redirect + - "--entrypoints.web.http.redirections.entryPoint.to=websecure" + - "--entrypoints.web.http.redirections.entryPoint.scheme=https" + # Docker Provider + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--providers.docker.network=insight-web" + # File Provider (fuer dynamische Konfiguration) + - "--providers.file.directory=/etc/traefik/dynamic" + - "--providers.file.watch=true" + # TLS (self-signed fuer Dev) + - "--entrypoints.websecure.http.tls=true" + # Logging + - "--log.level=INFO" + - "--accesslog=true" + - "--accesslog.format=json" + # Metrics fuer Prometheus + - "--metrics.prometheus=true" + - "--metrics.prometheus.entryPoint=metrics" + - "--entrypoints.metrics.address=:8082" + ports: + - "80:80" + - "443:443" + - "8080:8080" # Dashboard (nur intern) + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./config/traefik/dynamic:/etc/traefik/dynamic:ro + - traefik-certs:/certs + networks: + - insight-web + labels: + - "traefik.enable=true" + # Dashboard Route (nur intern) + - "traefik.http.routers.dashboard.rule=Host(`traefik.insight-dev.xinion.lan`)" + - "traefik.http.routers.dashboard.service=api@internal" + - "traefik.http.routers.dashboard.entrypoints=websecure" + healthcheck: + test: ["CMD", "traefik", "healthcheck"] + interval: 30s + timeout: 5s + retries: 3 + + # -------------------------------------------------------- + # PostgreSQL - Datenbank + # -------------------------------------------------------- + postgres: + image: postgres:16-alpine + container_name: insight-postgres + restart: unless-stopped + environment: + POSTGRES_USER: ${DB_USER:-insight} + POSTGRES_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD muss gesetzt sein} + POSTGRES_DB: ${DB_NAME:-platform_core} + # Performance-Tuning fuer 8GB RAM VM + POSTGRES_INITDB_ARGS: "--data-checksums" + volumes: + - postgres-data:/var/lib/postgresql/data + - ./config/postgres/init:/docker-entrypoint-initdb.d:ro + networks: + - insight-db + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-insight} -d ${DB_NAME:-platform_core}"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + # Performance-Tuning via Command + command: + - "postgres" + - "-c" + - "shared_buffers=2GB" + - "-c" + - "effective_cache_size=6GB" + - "-c" + - "work_mem=16MB" + - "-c" + - "maintenance_work_mem=512MB" + - "-c" + - "max_connections=200" + - "-c" + - "log_min_duration_statement=500" + - "-c" + - "log_statement=ddl" + + # -------------------------------------------------------- + # PgBouncer - Connection Pooling + # -------------------------------------------------------- + pgbouncer: + image: edoburu/pgbouncer:latest + container_name: insight-pgbouncer + restart: unless-stopped + environment: + DATABASE_URL: "postgres://${DB_USER:-insight}:${DB_PASSWORD}@postgres:5432/${DB_NAME:-platform_core}" + POOL_MODE: transaction + MAX_CLIENT_CONN: 500 + DEFAULT_POOL_SIZE: 25 + MIN_POOL_SIZE: 5 + RESERVE_POOL_SIZE: 5 + SERVER_RESET_QUERY: "DISCARD ALL" + SERVER_CHECK_DELAY: 30 + SERVER_CHECK_QUERY: "SELECT 1" + AUTH_TYPE: scram-sha-256 + networks: + - insight-db + depends_on: + postgres: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -p 6432"] + interval: 10s + timeout: 5s + retries: 3 + + # -------------------------------------------------------- + # Redis - Cache, Sessions, Event Bus + # -------------------------------------------------------- + redis: + image: redis:7-alpine + container_name: insight-redis + restart: unless-stopped + command: > + redis-server + --requirepass ${REDIS_PASSWORD:-} + --maxmemory 512mb + --maxmemory-policy allkeys-lru + --appendonly yes + --appendfsync everysec + --save 60 1000 + --save 300 100 + volumes: + - redis-data:/data + networks: + - insight-cache + healthcheck: + test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-}", "ping"] + interval: 10s + timeout: 5s + retries: 3 + + # -------------------------------------------------------- + # step-ca - Interne Certificate Authority (mTLS) + # -------------------------------------------------------- + step-ca: + image: smallstep/step-ca:latest + container_name: insight-step-ca + restart: unless-stopped + environment: + DOCKER_STEPCA_INIT_NAME: "INSIGHT Internal CA" + DOCKER_STEPCA_INIT_DNS_NAMES: "step-ca,localhost" + DOCKER_STEPCA_INIT_REMOTE_MANAGEMENT: "true" + DOCKER_STEPCA_INIT_ACME: "true" + volumes: + - step-ca-data:/home/step + networks: + - insight-web + - insight-db + - insight-cache + healthcheck: + test: ["CMD", "step", "ca", "health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + + # -------------------------------------------------------- + # Core-Service - NestJS Backend + # -------------------------------------------------------- + core: + build: + context: ./packages/core-service + dockerfile: Dockerfile + target: development + container_name: insight-core + restart: unless-stopped + environment: + NODE_ENV: ${NODE_ENV:-development} + APP_PORT: ${APP_PORT:-3000} + APP_URL: ${APP_URL:-https://insight-dev.xinion.lan} + FRONTEND_URL: ${FRONTEND_URL:-https://insight-dev.xinion.lan} + LOG_LEVEL: ${LOG_LEVEL:-info} + # Database (via PgBouncer) + DATABASE_URL: "postgresql://${DB_USER:-insight}:${DB_PASSWORD}@pgbouncer:6432/${DB_NAME:-platform_core}" + # Database (direkt fuer Migrations) + DATABASE_URL_DIRECT: "postgresql://${DB_USER:-insight}:${DB_PASSWORD}@postgres:5432/${DB_NAME:-platform_core}" + # Redis + REDIS_HOST: redis + REDIS_PORT: 6379 + REDIS_PASSWORD: ${REDIS_PASSWORD:-} + # JWT + JWT_PRIVATE_KEY_PATH: ${JWT_PRIVATE_KEY_PATH:-/app/keys/jwt-private.pem} + JWT_PUBLIC_KEY_PATH: ${JWT_PUBLIC_KEY_PATH:-/app/keys/jwt-public.pem} + JWT_ACCESS_TOKEN_EXPIRY: ${JWT_ACCESS_TOKEN_EXPIRY:-15m} + JWT_REFRESH_TOKEN_EXPIRY: ${JWT_REFRESH_TOKEN_EXPIRY:-7d} + JWT_ISSUER: ${JWT_ISSUER:-insight-platform} + # Bcrypt + BCRYPT_COST: ${BCRYPT_COST:-12} + # CORS + CORS_ORIGINS: ${CORS_ORIGINS:-https://insight-dev.xinion.lan} + # Rate Limiting + THROTTLE_TTL: ${THROTTLE_TTL:-60000} + THROTTLE_LIMIT: ${THROTTLE_LIMIT:-200} + networks: + - insight-web + - insight-db + - insight-cache + depends_on: + pgbouncer: + condition: service_healthy + redis: + condition: service_healthy + labels: + - "traefik.enable=true" + # API Routing + - "traefik.http.routers.core-api.rule=Host(`insight-dev.xinion.lan`) && PathPrefix(`/api`)" + - "traefik.http.routers.core-api.entrypoints=websecure" + - "traefik.http.routers.core-api.service=core-api" + - "traefik.http.services.core-api.loadbalancer.server.port=3000" + # Health-Endpunkt (ohne Auth) + - "traefik.http.routers.core-health.rule=Host(`insight-dev.xinion.lan`) && Path(`/health`)" + - "traefik.http.routers.core-health.entrypoints=websecure" + - "traefik.http.routers.core-health.service=core-api" + # Rate Limiting Middleware + - "traefik.http.middlewares.api-ratelimit.ratelimit.average=100" + - "traefik.http.middlewares.api-ratelimit.ratelimit.burst=50" + - "traefik.http.routers.core-api.middlewares=api-ratelimit" + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"] + interval: 15s + timeout: 5s + retries: 3 + start_period: 30s + + # -------------------------------------------------------- + # Frontend - React App (via Nginx) + # -------------------------------------------------------- + frontend: + build: + context: ./packages/frontend + dockerfile: Dockerfile + target: development + container_name: insight-frontend + restart: unless-stopped + networks: + - insight-web + labels: + - "traefik.enable=true" + # Frontend Routing (Catch-All nach API) + - "traefik.http.routers.frontend.rule=Host(`insight-dev.xinion.lan`)" + - "traefik.http.routers.frontend.entrypoints=websecure" + - "traefik.http.routers.frontend.service=frontend" + - "traefik.http.routers.frontend.priority=1" + - "traefik.http.services.frontend.loadbalancer.server.port=8080" + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8080/ || exit 1"] + interval: 30s + timeout: 5s + retries: 3 diff --git a/packages/core-service/.dockerignore b/packages/core-service/.dockerignore new file mode 100644 index 0000000..a335510 --- /dev/null +++ b/packages/core-service/.dockerignore @@ -0,0 +1,7 @@ +node_modules +dist +coverage +.env +*.md +.git +.gitignore diff --git a/packages/core-service/Dockerfile b/packages/core-service/Dockerfile new file mode 100644 index 0000000..83f288c --- /dev/null +++ b/packages/core-service/Dockerfile @@ -0,0 +1,56 @@ +# ============================================================ +# INSIGHT Core-Service - Multi-Stage Dockerfile +# ============================================================ + +# --- Base Stage --- +FROM node:20-alpine AS base +WORKDIR /app +RUN apk add --no-cache openssl + +# --- Dependencies Stage --- +FROM base AS deps +COPY package.json package-lock.json* ./ +RUN npm ci --ignore-scripts +# Prisma Generate braucht die Schema-Dateien +COPY prisma ./prisma +RUN npx prisma generate --schema=prisma/core.schema.prisma + +# --- Development Stage --- +FROM base AS development +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN npx prisma generate --schema=prisma/core.schema.prisma +EXPOSE 3000 +CMD ["npm", "run", "start:dev"] + +# --- Build Stage --- +FROM base AS build +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN npm run build + +# --- Production Stage --- +FROM base AS production +WORKDIR /app +ENV NODE_ENV=production + +# Nur Produktions-Dependencies +COPY package.json package-lock.json* ./ +RUN npm ci --omit=dev --ignore-scripts + +# Prisma Client generieren +COPY prisma ./prisma +RUN npx prisma generate --schema=prisma/core.schema.prisma + +# Kompilierter Code +COPY --from=build /app/dist ./dist + +# Non-root User +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nestjs -u 1001 -G nodejs +USER nestjs + +EXPOSE 3000 +CMD ["node", "dist/main"] diff --git a/packages/core-service/nest-cli.json b/packages/core-service/nest-cli.json new file mode 100644 index 0000000..f9aa683 --- /dev/null +++ b/packages/core-service/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/packages/core-service/package.json b/packages/core-service/package.json new file mode 100644 index 0000000..43517c4 --- /dev/null +++ b/packages/core-service/package.json @@ -0,0 +1,93 @@ +{ + "name": "@insight/core-service", + "version": "0.1.0", + "description": "INSIGHT MVP - Core Service (NestJS Backend)", + "private": true, + "license": "UNLICENSED", + "scripts": { + "build": "nest build", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/main", + "lint": "eslint \"{src,test}/**/*.ts\" --fix", + "lint:check": "eslint \"{src,test}/**/*.ts\"", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "format:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\"", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "test:e2e": "jest --config ./test/jest-e2e.json", + "typecheck": "tsc --noEmit", + "prisma:generate": "prisma generate --schema=prisma/core.schema.prisma", + "prisma:migrate": "prisma migrate dev --schema=prisma/core.schema.prisma", + "prisma:migrate:deploy": "prisma migrate deploy --schema=prisma/core.schema.prisma", + "prisma:studio": "prisma studio --schema=prisma/core.schema.prisma" + }, + "dependencies": { + "@nestjs/common": "^10.4.0", + "@nestjs/config": "^3.2.0", + "@nestjs/core": "^10.4.0", + "@nestjs/jwt": "^10.2.0", + "@nestjs/passport": "^10.0.3", + "@nestjs/platform-express": "^10.4.0", + "@nestjs/schedule": "^4.1.0", + "@nestjs/swagger": "^7.4.0", + "@nestjs/throttler": "^6.2.0", + "@prisma/client": "^6.4.0", + "bcrypt": "^5.1.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "cookie-parser": "^1.4.7", + "helmet": "^8.0.0", + "ioredis": "^5.4.1", + "otplib": "^12.0.1", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "qrcode": "^1.5.4", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "uuid": "^10.0.0" + }, + "devDependencies": { + "@nestjs/cli": "^10.4.0", + "@nestjs/schematics": "^10.1.0", + "@nestjs/testing": "^10.4.0", + "@types/bcrypt": "^5.0.2", + "@types/cookie-parser": "^1.4.7", + "@types/express": "^5.0.0", + "@types/jest": "^29.5.12", + "@types/node": "^22.0.0", + "@types/passport-jwt": "^4.0.1", + "@types/qrcode": "^1.5.5", + "@types/uuid": "^10.0.0", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", + "eslint": "^9.0.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.0", + "jest": "^29.7.0", + "prettier": "^3.3.0", + "prisma": "^6.4.0", + "source-map-support": "^0.5.21", + "ts-jest": "^29.2.0", + "ts-loader": "^9.5.0", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.6.0" + }, + "jest": { + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": "src", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "collectCoverageFrom": ["**/*.(t|j)s"], + "coverageDirectory": "../coverage", + "testEnvironment": "node", + "moduleNameMapper": { + "^@/(.*)$": "/$1" + } + } +} diff --git a/packages/core-service/prisma/.gitkeep b/packages/core-service/prisma/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/packages/core-service/prisma/core.schema.prisma b/packages/core-service/prisma/core.schema.prisma new file mode 100644 index 0000000..94ff324 --- /dev/null +++ b/packages/core-service/prisma/core.schema.prisma @@ -0,0 +1,181 @@ +// ============================================================ +// INSIGHT MVP - Core Schema (platform_core Datenbank) +// ============================================================ +// Zentrale Plattform-Tabellen: Users, Tenants, Auth, Modules +// Kein raw SQL - nur Prisma! +// ============================================================ + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + directUrl = env("DATABASE_URL_DIRECT") +} + +// -------------------------------------------------------- +// User - Plattform-Benutzer +// -------------------------------------------------------- +model User { + id String @id @default(uuid()) @db.Uuid + email String @unique @db.VarChar(255) + firstName String @map("first_name") @db.VarChar(100) + lastName String @map("last_name") @db.VarChar(100) + role String @default("USER") @db.VarChar(50) // PLATFORM_ADMIN, TENANT_ADMIN, USER + isActive Boolean @default(true) @map("is_active") + + // 2FA + twoFactorEnabled Boolean @default(false) @map("two_factor_enabled") + + // Login-Tracking + lastLogin DateTime? @map("last_login") + failedLoginAttempts Int @default(0) @map("failed_login_attempts") + lastFailedLogin DateTime? @map("last_failed_login") + + // Timestamps + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + // Relationen + authProvider AuthProvider[] + tenantMemberships TenantMembership[] + auditLogs AuditLog[] + + @@map("users") +} + +// -------------------------------------------------------- +// AuthProvider - Authentifizierungs-Provider pro User +// -------------------------------------------------------- +// Unterstuetzt mehrere Auth-Methoden pro User: +// LOCAL (Passwort), MS_SSO (spaeter), M2M (Machine-to-Machine) +model AuthProvider { + id String @id @default(uuid()) @db.Uuid + userId String @map("user_id") @db.Uuid + provider String @db.VarChar(50) // LOCAL, MS_SSO, M2M + providerId String? @map("provider_id") @db.VarChar(255) // Externe ID (z.B. MS Object ID) + passwordHash String? @map("password_hash") @db.VarChar(255) + totpSecret String? @map("totp_secret") @db.VarChar(255) // Verschluesselt + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + // Relationen + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([userId, provider]) + @@map("auth_providers") +} + +// -------------------------------------------------------- +// Tenant - Mandant +// -------------------------------------------------------- +model Tenant { + id String @id @default(uuid()) @db.Uuid + name String @db.VarChar(200) + slug String @unique @db.VarChar(50) // URL-freundlich, fuer DB-Name + isActive Boolean @default(true) @map("is_active") + + // Mandant-Einstellungen (JSON) + settings Json @default("{}") + + // Timestamps + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + // Relationen + members TenantMembership[] + modules TenantModule[] + + @@map("tenants") +} + +// -------------------------------------------------------- +// TenantMembership - User-Tenant-Zuordnung (M:N) +// -------------------------------------------------------- +model TenantMembership { + id String @id @default(uuid()) @db.Uuid + userId String @map("user_id") @db.Uuid + tenantId String @map("tenant_id") @db.Uuid + tenantRole String @default("MEMBER") @map("tenant_role") @db.VarChar(50) // ADMIN, MEMBER, VIEWER + isActive Boolean @default(true) @map("is_active") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + // Relationen + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + + @@unique([userId, tenantId]) + @@map("tenant_memberships") +} + +// -------------------------------------------------------- +// Module - Verfuegbare Plattform-Module +// -------------------------------------------------------- +model Module { + id String @id @default(uuid()) @db.Uuid + key String @unique @db.VarChar(50) // z.B. "crm", "project", "docs" + name String @db.VarChar(100) + description String? @db.Text + version String @default("1.0.0") @db.VarChar(20) + isActive Boolean @default(true) @map("is_active") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + // Relationen + tenantModules TenantModule[] + + @@map("modules") +} + +// -------------------------------------------------------- +// TenantModule - Welcher Tenant welche Module nutzt +// -------------------------------------------------------- +model TenantModule { + id String @id @default(uuid()) @db.Uuid + tenantId String @map("tenant_id") @db.Uuid + moduleId String @map("module_id") @db.Uuid + isActive Boolean @default(true) @map("is_active") + + // Modul-spezifische Konfiguration pro Tenant + config Json @default("{}") + + activatedAt DateTime @default(now()) @map("activated_at") + + // Relationen + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + module Module @relation(fields: [moduleId], references: [id], onDelete: Cascade) + + @@unique([tenantId, moduleId]) + @@map("tenant_modules") +} + +// -------------------------------------------------------- +// AuditLog - Plattform-weites Audit-Log +// -------------------------------------------------------- +model AuditLog { + id String @id @default(uuid()) @db.Uuid + userId String? @map("user_id") @db.Uuid + action String @db.VarChar(100) // z.B. "user.login", "tenant.create" + entity String @db.VarChar(100) // z.B. "User", "Tenant" + entityId String? @map("entity_id") @db.VarChar(255) + details Json? // Zusaetzliche Informationen + ipAddress String? @map("ip_address") @db.VarChar(45) + userAgent String? @map("user_agent") @db.Text + + createdAt DateTime @default(now()) @map("created_at") + + // Relationen + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + + @@index([userId]) + @@index([action]) + @@index([entity, entityId]) + @@index([createdAt]) + @@map("audit_logs") +} diff --git a/packages/core-service/prisma/tenant.schema.prisma b/packages/core-service/prisma/tenant.schema.prisma new file mode 100644 index 0000000..ed495d0 --- /dev/null +++ b/packages/core-service/prisma/tenant.schema.prisma @@ -0,0 +1,111 @@ +// ============================================================ +// INSIGHT MVP - Tenant Schema (tenant_{slug} Datenbanken) +// ============================================================ +// Jeder Mandant hat eine eigene Datenbank mit diesen Tabellen. +// Schema wird per Prisma Migrate auf neue Tenant-DBs angewandt. +// +// HINWEIS: Dieses Schema wird derzeit als Referenz gefuehrt. +// Die tatsaechliche Migration auf Tenant-DBs erfolgt +// in Sprint 2+ wenn das CRM-Modul implementiert wird. +// ============================================================ + +generator client { + provider = "prisma-client-js" + output = "../node_modules/.prisma/tenant-client" +} + +datasource db { + provider = "postgresql" + url = env("TENANT_DATABASE_URL") +} + +// -------------------------------------------------------- +// Contact - CRM-Kontakte (Personen & Organisationen) +// -------------------------------------------------------- +model Contact { + id String @id @default(uuid()) @db.Uuid + type ContactType @default(PERSON) + + // Person + firstName String? @map("first_name") @db.VarChar(100) + lastName String? @map("last_name") @db.VarChar(100) + + // Organisation + companyName String? @map("company_name") @db.VarChar(200) + + // Kontaktdaten + email String? @db.VarChar(255) + phone String? @db.VarChar(50) + mobile String? @db.VarChar(50) + website String? @db.VarChar(500) + + // Adresse + street String? @db.VarChar(200) + zip String? @db.VarChar(20) + city String? @db.VarChar(100) + state String? @db.VarChar(100) + country String? @default("DE") @db.VarChar(2) + + // Zusaetzlich + notes String? @db.Text + tags String[] @default([]) + + isActive Boolean @default(true) @map("is_active") + + // Wer hat erstellt/bearbeitet (User-IDs aus platform_core) + createdBy String @map("created_by") @db.Uuid + updatedBy String? @map("updated_by") @db.Uuid + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + // Relationen + activities Activity[] + + @@index([email]) + @@index([companyName]) + @@index([lastName, firstName]) + @@map("contacts") +} + +enum ContactType { + PERSON + ORGANIZATION +} + +// -------------------------------------------------------- +// Activity - CRM-Aktivitaeten (Notizen, Anrufe, E-Mails) +// -------------------------------------------------------- +model Activity { + id String @id @default(uuid()) @db.Uuid + contactId String @map("contact_id") @db.Uuid + type ActivityType + subject String @db.VarChar(500) + description String? @db.Text + + // Terminierung + scheduledAt DateTime? @map("scheduled_at") + completedAt DateTime? @map("completed_at") + + // Wer hat erstellt (User-ID aus platform_core) + createdBy String @map("created_by") @db.Uuid + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + // Relationen + contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade) + + @@index([contactId]) + @@index([type]) + @@index([scheduledAt]) + @@map("activities") +} + +enum ActivityType { + NOTE + CALL + EMAIL + MEETING + TASK +} diff --git a/packages/core-service/src/app.module.ts b/packages/core-service/src/app.module.ts new file mode 100644 index 0000000..c86c293 --- /dev/null +++ b/packages/core-service/src/app.module.ts @@ -0,0 +1,57 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { APP_GUARD } from '@nestjs/core'; +import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler'; +import { ScheduleModule } from '@nestjs/schedule'; +import { HealthModule } from './health/health.module'; +import { PrismaModule } from './prisma/prisma.module'; +import { RedisModule } from './redis/redis.module'; +import { AuthModule } from './core/auth/auth.module'; +import { UsersModule } from './core/users/users.module'; +import { TenantsModule } from './core/tenants/tenants.module'; +import { JwtAuthGuard } from './common/guards/jwt-auth.guard'; +import { validateConfig } from './config/env.validation'; + +@Module({ + imports: [ + // Konfiguration (.env) + ConfigModule.forRoot({ + isGlobal: true, + validate: validateConfig, + }), + + // Rate Limiting + ThrottlerModule.forRoot([ + { + ttl: parseInt(process.env.THROTTLE_TTL ?? '60000', 10), + limit: parseInt(process.env.THROTTLE_LIMIT ?? '200', 10), + }, + ]), + + // Cron-Jobs + ScheduleModule.forRoot(), + + // Infrastruktur-Module + PrismaModule, + RedisModule, + + // Feature-Module + HealthModule, + AuthModule, + UsersModule, + TenantsModule, + ], + providers: [ + // Global Guards: Alle Routen sind standardmaessig geschuetzt + // Oeffentliche Routen muessen mit @Public() dekoriert werden + { + provide: APP_GUARD, + useClass: JwtAuthGuard, + }, + { + provide: APP_GUARD, + useClass: ThrottlerGuard, + }, + ], +}) +export class AppModule {} diff --git a/packages/core-service/src/common/decorators/.gitkeep b/packages/core-service/src/common/decorators/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/packages/core-service/src/common/decorators/current-user.decorator.ts b/packages/core-service/src/common/decorators/current-user.decorator.ts new file mode 100644 index 0000000..b77a12c --- /dev/null +++ b/packages/core-service/src/common/decorators/current-user.decorator.ts @@ -0,0 +1,35 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { Request } from 'express'; + +export interface JwtPayload { + sub: string; // User-ID + email: string; + role: string; + tenantId?: string; + tenantSlug?: string; + jti: string; // Token-ID fuer Revocation + iat: number; + exp: number; +} + +/** + * @CurrentUser() - Extrahiert den authentifizierten User aus dem Request. + * + * Beispiel: + * @Get('profile') + * getProfile(@CurrentUser() user: JwtPayload) { + * return user; + * } + * + * @Get('profile/id') + * getProfileId(@CurrentUser('sub') userId: string) { + * return userId; + * } + */ +export const CurrentUser = createParamDecorator( + (data: keyof JwtPayload | undefined, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + const user = request.user as JwtPayload; + return data ? user?.[data] : user; + }, +); diff --git a/packages/core-service/src/common/decorators/index.ts b/packages/core-service/src/common/decorators/index.ts new file mode 100644 index 0000000..7af6862 --- /dev/null +++ b/packages/core-service/src/common/decorators/index.ts @@ -0,0 +1,3 @@ +export { Public, IS_PUBLIC_KEY } from './public.decorator'; +export { Roles, ROLES_KEY } from './roles.decorator'; +export { CurrentUser, type JwtPayload } from './current-user.decorator'; diff --git a/packages/core-service/src/common/decorators/public.decorator.ts b/packages/core-service/src/common/decorators/public.decorator.ts new file mode 100644 index 0000000..8fe5adf --- /dev/null +++ b/packages/core-service/src/common/decorators/public.decorator.ts @@ -0,0 +1,16 @@ +import { SetMetadata } from '@nestjs/common'; + +export const IS_PUBLIC_KEY = 'isPublic'; + +/** + * @Public() - Markiert eine Route als oeffentlich zugaenglich. + * + * Standardmaessig sind ALLE Routen durch den JwtAuthGuard geschuetzt. + * Nur explizit mit @Public() dekorierte Routen sind ohne Token erreichbar. + * + * Beispiel: + * @Get('health') + * @Public() + * healthCheck() { ... } + */ +export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); diff --git a/packages/core-service/src/common/decorators/roles.decorator.ts b/packages/core-service/src/common/decorators/roles.decorator.ts new file mode 100644 index 0000000..621f724 --- /dev/null +++ b/packages/core-service/src/common/decorators/roles.decorator.ts @@ -0,0 +1,13 @@ +import { SetMetadata } from '@nestjs/common'; + +export const ROLES_KEY = 'roles'; + +/** + * @Roles() - Beschraenkt den Zugriff auf bestimmte Plattform-Rollen. + * + * Beispiel: + * @Roles('PLATFORM_ADMIN', 'TENANT_ADMIN') + * @Get('admin/users') + * listUsers() { ... } + */ +export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles); diff --git a/packages/core-service/src/common/filters/.gitkeep b/packages/core-service/src/common/filters/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/packages/core-service/src/common/filters/global-exception.filter.ts b/packages/core-service/src/common/filters/global-exception.filter.ts new file mode 100644 index 0000000..baa76c2 --- /dev/null +++ b/packages/core-service/src/common/filters/global-exception.filter.ts @@ -0,0 +1,88 @@ +import { + ExceptionFilter, + Catch, + ArgumentsHost, + HttpException, + HttpStatus, + Logger, +} from '@nestjs/common'; +import { Request, Response } from 'express'; + +interface ErrorResponse { + statusCode: number; + message: string; + error: string; + timestamp: string; + path: string; + requestId?: string; +} + +/** + * GlobalExceptionFilter - Faengt alle unbehandelten Exceptions. + * + * - Strukturierte Fehlerantworten im JSON-Format + * - Logging aller Fehler + * - Keine internen Details in Produktions-Fehlern + */ +@Catch() +export class GlobalExceptionFilter implements ExceptionFilter { + private readonly logger = new Logger(GlobalExceptionFilter.name); + + catch(exception: unknown, host: ArgumentsHost): void { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + + let statusCode: number; + let message: string; + let error: string; + + if (exception instanceof HttpException) { + statusCode = exception.getStatus(); + const exceptionResponse = exception.getResponse(); + + if (typeof exceptionResponse === 'string') { + message = exceptionResponse; + error = exception.name; + } else if (typeof exceptionResponse === 'object') { + const resp = exceptionResponse as Record; + message = Array.isArray(resp.message) + ? resp.message.join(', ') + : (resp.message as string) || exception.message; + error = (resp.error as string) || exception.name; + } else { + message = exception.message; + error = exception.name; + } + } else if (exception instanceof Error) { + statusCode = HttpStatus.INTERNAL_SERVER_ERROR; + message = + process.env.NODE_ENV === 'production' + ? 'Interner Serverfehler' + : exception.message; + error = 'InternalServerError'; + + // Stack-Trace loggen fuer unerwartete Fehler + this.logger.error( + `Unbehandelter Fehler: ${exception.message}`, + exception.stack, + ); + } else { + statusCode = HttpStatus.INTERNAL_SERVER_ERROR; + message = 'Unbekannter Fehler'; + error = 'UnknownError'; + this.logger.error('Unbekannter Fehler:', exception); + } + + const errorResponse: ErrorResponse = { + statusCode, + message, + error, + timestamp: new Date().toISOString(), + path: request.url, + requestId: request.headers['x-request-id'] as string | undefined, + }; + + response.status(statusCode).json(errorResponse); + } +} diff --git a/packages/core-service/src/common/guards/.gitkeep b/packages/core-service/src/common/guards/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/packages/core-service/src/common/guards/jwt-auth.guard.ts b/packages/core-service/src/common/guards/jwt-auth.guard.ts new file mode 100644 index 0000000..d7bd762 --- /dev/null +++ b/packages/core-service/src/common/guards/jwt-auth.guard.ts @@ -0,0 +1,65 @@ +import { + Injectable, + ExecutionContext, + UnauthorizedException, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { AuthGuard } from '@nestjs/passport'; +import { IS_PUBLIC_KEY } from '../decorators/public.decorator'; +import { RedisService } from '../../redis/redis.service'; +import { JwtPayload } from '../decorators/current-user.decorator'; + +/** + * JwtAuthGuard - Globaler Guard fuer JWT-Authentifizierung. + * + * - Standardmaessig aktiv auf ALLEN Routen + * - @Public() dekorierte Routen werden uebersprungen + * - Prueft zusaetzlich ob der Token revoked wurde (Redis Blocklist) + */ +@Injectable() +export class JwtAuthGuard extends AuthGuard('jwt') { + constructor( + private readonly reflector: Reflector, + private readonly redis: RedisService, + ) { + super(); + } + + async canActivate(context: ExecutionContext): Promise { + // @Public() Routen ueberspringen + const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ + context.getHandler(), + context.getClass(), + ]); + + if (isPublic) { + return true; + } + + // JWT validieren (Passport Strategy) + const canActivate = await super.canActivate(context); + if (!canActivate) { + return false; + } + + // Token-Revocation pruefen (Redis Blocklist) + const request = context.switchToHttp().getRequest(); + const user = request.user as JwtPayload; + + if (user?.jti) { + const isBlocked = await this.redis.isTokenBlocked(user.jti); + if (isBlocked) { + throw new UnauthorizedException('Token wurde widerrufen'); + } + } + + return true; + } + + handleRequest(err: Error | null, user: T, info: Error | undefined): T { + if (err || !user) { + throw err || new UnauthorizedException('Zugriff verweigert'); + } + return user; + } +} diff --git a/packages/core-service/src/common/guards/roles.guard.ts b/packages/core-service/src/common/guards/roles.guard.ts new file mode 100644 index 0000000..9f68b2b --- /dev/null +++ b/packages/core-service/src/common/guards/roles.guard.ts @@ -0,0 +1,37 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { ROLES_KEY } from '../decorators/roles.decorator'; +import { JwtPayload } from '../decorators/current-user.decorator'; + +/** + * RolesGuard - Prueft ob der User die erforderliche Rolle hat. + * + * Wird zusammen mit @Roles() verwendet: + * @Roles('PLATFORM_ADMIN') + * @UseGuards(RolesGuard) + * @Get('admin/dashboard') + */ +@Injectable() +export class RolesGuard implements CanActivate { + constructor(private readonly reflector: Reflector) {} + + canActivate(context: ExecutionContext): boolean { + const requiredRoles = this.reflector.getAllAndOverride( + ROLES_KEY, + [context.getHandler(), context.getClass()], + ); + + if (!requiredRoles || requiredRoles.length === 0) { + return true; + } + + const request = context.switchToHttp().getRequest(); + const user = request.user as JwtPayload; + + if (!user?.role) { + return false; + } + + return requiredRoles.includes(user.role); + } +} diff --git a/packages/core-service/src/common/interceptors/.gitkeep b/packages/core-service/src/common/interceptors/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/packages/core-service/src/config/.gitkeep b/packages/core-service/src/config/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/packages/core-service/src/config/env.validation.ts b/packages/core-service/src/config/env.validation.ts new file mode 100644 index 0000000..65dc142 --- /dev/null +++ b/packages/core-service/src/config/env.validation.ts @@ -0,0 +1,98 @@ +import { plainToInstance } from 'class-transformer'; +import { + IsEnum, + IsNotEmpty, + IsNumber, + IsOptional, + IsString, + Min, + Max, + validateSync, +} from 'class-validator'; + +enum Environment { + Development = 'development', + Production = 'production', + Test = 'test', +} + +class EnvironmentVariables { + @IsEnum(Environment) + NODE_ENV: Environment = Environment.Development; + + @IsNumber() + @Min(1) + @Max(65535) + APP_PORT = 3000; + + @IsString() + @IsNotEmpty() + APP_URL = 'https://insight-dev.xinion.lan'; + + // Datenbank + @IsString() + @IsNotEmpty() + DATABASE_URL!: string; + + @IsString() + @IsOptional() + DATABASE_URL_DIRECT?: string; + + // Redis + @IsString() + REDIS_HOST = 'redis'; + + @IsNumber() + REDIS_PORT = 6379; + + @IsString() + @IsOptional() + REDIS_PASSWORD?: string; + + // JWT + @IsString() + @IsNotEmpty() + JWT_PRIVATE_KEY_PATH = '/app/keys/jwt-private.pem'; + + @IsString() + @IsNotEmpty() + JWT_PUBLIC_KEY_PATH = '/app/keys/jwt-public.pem'; + + @IsString() + JWT_ACCESS_TOKEN_EXPIRY = '15m'; + + @IsString() + JWT_REFRESH_TOKEN_EXPIRY = '7d'; + + @IsString() + JWT_ISSUER = 'insight-platform'; + + // Bcrypt + @IsNumber() + @Min(10) + @Max(14) + BCRYPT_COST = 12; + + // Rate Limiting + @IsNumber() + THROTTLE_TTL = 60000; + + @IsNumber() + THROTTLE_LIMIT = 200; +} + +export function validateConfig( + config: Record, +): EnvironmentVariables { + const validatedConfig = plainToInstance(EnvironmentVariables, config, { + enableImplicitConversion: true, + }); + const errors = validateSync(validatedConfig, { + skipMissingProperties: false, + }); + + if (errors.length > 0) { + throw new Error(`Config validation error: ${errors.toString()}`); + } + return validatedConfig; +} diff --git a/packages/core-service/src/core/auth/.gitkeep b/packages/core-service/src/core/auth/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/packages/core-service/src/core/auth/auth.controller.ts b/packages/core-service/src/core/auth/auth.controller.ts new file mode 100644 index 0000000..a71493d --- /dev/null +++ b/packages/core-service/src/core/auth/auth.controller.ts @@ -0,0 +1,121 @@ +import { + Controller, + Post, + Body, + Res, + Req, + HttpCode, + HttpStatus, +} from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; +import { Request, Response } from 'express'; +import { Public } from '../../common/decorators/public.decorator'; +import { CurrentUser, JwtPayload } from '../../common/decorators/current-user.decorator'; +import { AuthService } from './auth.service'; +import { LoginDto } from './dto/login.dto'; + +@ApiTags('Authentifizierung') +@Controller('auth') +export class AuthController { + constructor(private readonly authService: AuthService) {} + + /** + * POST /api/v1/auth/login + * Login mit E-Mail + Passwort (+ optionaler TOTP-Code). + */ + @Post('login') + @Public() + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Login mit E-Mail und Passwort' }) + async login( + @Body() dto: LoginDto, + @Res({ passthrough: true }) res: Response, + ) { + const result = await this.authService.login(dto); + + // 2FA erforderlich - kein Token setzen + if (result.requiresTwoFactor) { + return { + requiresTwoFactor: true, + message: 'Bitte 2FA-Code eingeben', + }; + } + + // Refresh-Token als HttpOnly Cookie setzen (NICHT im localStorage!) + // Regel: Kein localStorage fuer Tokens + this.setRefreshTokenCookie(res, result.accessToken); + + return { + accessToken: result.accessToken, + user: result.user, + }; + } + + /** + * POST /api/v1/auth/refresh + * Token-Refresh via HttpOnly Cookie. + */ + @Post('refresh') + @Public() + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Access-Token erneuern (Silent Refresh)' }) + async refresh( + @Req() req: Request, + @Res({ passthrough: true }) res: Response, + ) { + const refreshToken = req.cookies?.refresh_token as string | undefined; + if (!refreshToken) { + res.status(HttpStatus.UNAUTHORIZED).json({ + message: 'Kein Refresh-Token vorhanden', + }); + return; + } + + const tokens = await this.authService.refreshTokens(refreshToken); + this.setRefreshTokenCookie(res, tokens.refreshToken); + + return { + accessToken: tokens.accessToken, + }; + } + + /** + * POST /api/v1/auth/logout + * Logout: Tokens invalidieren, Cookie loeschen. + */ + @Post('logout') + @HttpCode(HttpStatus.OK) + @ApiBearerAuth('access-token') + @ApiOperation({ summary: 'Logout und Token-Invalidierung' }) + async logout( + @CurrentUser() user: JwtPayload, + @Req() req: Request, + @Res({ passthrough: true }) res: Response, + ) { + const refreshToken = req.cookies?.refresh_token as string | undefined; + await this.authService.logout(user, refreshToken); + + // Refresh-Token Cookie loeschen + res.clearCookie('refresh_token', { + httpOnly: true, + secure: true, + sameSite: 'strict', + path: '/api/v1/auth', + }); + + return { message: 'Erfolgreich abgemeldet' }; + } + + /** + * Setzt das Refresh-Token als HttpOnly, Secure, SameSite=Strict Cookie. + */ + private setRefreshTokenCookie(res: Response, refreshToken: string): void { + res.cookie('refresh_token', refreshToken, { + httpOnly: true, + secure: true, // Nur HTTPS + sameSite: 'strict', + path: '/api/v1/auth', // Nur fuer Auth-Endpunkte + maxAge: 7 * 24 * 60 * 60 * 1000, // 7 Tage + }); + } +} diff --git a/packages/core-service/src/core/auth/auth.module.ts b/packages/core-service/src/core/auth/auth.module.ts new file mode 100644 index 0000000..23a19aa --- /dev/null +++ b/packages/core-service/src/core/auth/auth.module.ts @@ -0,0 +1,47 @@ +import { Module } from '@nestjs/common'; +import { JwtModule } from '@nestjs/jwt'; +import { PassportModule } from '@nestjs/passport'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import * as fs from 'fs'; +import { AuthController } from './auth.controller'; +import { AuthService } from './auth.service'; +import { JwtStrategy } from './strategies/jwt.strategy'; +import { TotpService } from './totp.service'; + +@Module({ + imports: [ + PassportModule.register({ defaultStrategy: 'jwt' }), + JwtModule.registerAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (config: ConfigService) => { + const privateKeyPath = config.get( + 'JWT_PRIVATE_KEY_PATH', + '/app/keys/jwt-private.pem', + ); + const publicKeyPath = config.get( + 'JWT_PUBLIC_KEY_PATH', + '/app/keys/jwt-public.pem', + ); + + return { + privateKey: fs.readFileSync(privateKeyPath, 'utf8'), + publicKey: fs.readFileSync(publicKeyPath, 'utf8'), + signOptions: { + algorithm: 'RS256', + issuer: config.get('JWT_ISSUER', 'insight-platform'), + expiresIn: config.get('JWT_ACCESS_TOKEN_EXPIRY', '15m'), + }, + verifyOptions: { + algorithms: ['RS256'], + issuer: config.get('JWT_ISSUER', 'insight-platform'), + }, + }; + }, + }), + ], + controllers: [AuthController], + providers: [AuthService, JwtStrategy, TotpService], + exports: [AuthService, JwtModule], +}) +export class AuthModule {} diff --git a/packages/core-service/src/core/auth/auth.service.ts b/packages/core-service/src/core/auth/auth.service.ts new file mode 100644 index 0000000..82bd4c1 --- /dev/null +++ b/packages/core-service/src/core/auth/auth.service.ts @@ -0,0 +1,264 @@ +import { + Injectable, + UnauthorizedException, + ForbiddenException, + Logger, +} from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { ConfigService } from '@nestjs/config'; +import * as bcrypt from 'bcrypt'; +import { v4 as uuidv4 } from 'uuid'; +import { PrismaService } from '../../prisma/prisma.service'; +import { RedisService } from '../../redis/redis.service'; +import { TotpService } from './totp.service'; +import { LoginDto } from './dto/login.dto'; +import { JwtPayload } from '../../common/decorators/current-user.decorator'; + +interface TokenPair { + accessToken: string; + refreshToken: string; +} + +interface LoginResponse { + accessToken: string; + user: { + id: string; + email: string; + firstName: string; + lastName: string; + role: string; + }; + requiresTwoFactor?: boolean; +} + +@Injectable() +export class AuthService { + private readonly logger = new Logger(AuthService.name); + + constructor( + private readonly prisma: PrismaService, + private readonly jwt: JwtService, + private readonly redis: RedisService, + private readonly config: ConfigService, + private readonly totp: TotpService, + ) {} + + /** + * Login mit E-Mail und Passwort. + * Gibt AccessToken + Refresh-Token (HttpOnly Cookie) zurueck. + */ + async login(dto: LoginDto): Promise { + // User finden + const user = await this.prisma.user.findUnique({ + where: { email: dto.email.toLowerCase() }, + include: { + authProvider: true, + tenantMemberships: { + include: { tenant: true }, + where: { isActive: true }, + take: 1, + }, + }, + }); + + if (!user || !user.isActive) { + // Generische Fehlermeldung (kein Hinweis ob User existiert) + throw new UnauthorizedException('Ungueltige Anmeldedaten'); + } + + // Passwort pruefen (nur fuer lokale Auth) + const localAuth = user.authProvider.find((ap) => ap.provider === 'LOCAL'); + if (!localAuth?.passwordHash) { + throw new UnauthorizedException('Ungueltige Anmeldedaten'); + } + + const passwordValid = await bcrypt.compare( + dto.password, + localAuth.passwordHash, + ); + if (!passwordValid) { + // Failed Login zaehlen + await this.prisma.user.update({ + where: { id: user.id }, + data: { + failedLoginAttempts: { increment: 1 }, + lastFailedLogin: new Date(), + }, + }); + throw new UnauthorizedException('Ungueltige Anmeldedaten'); + } + + // Account-Sperre pruefen (nach 5 Fehlversuchen) + if (user.failedLoginAttempts >= 5) { + const lockoutEnd = user.lastFailedLogin + ? new Date(user.lastFailedLogin.getTime() + 15 * 60 * 1000) // 15 Min Sperre + : null; + + if (lockoutEnd && lockoutEnd > new Date()) { + throw new ForbiddenException( + 'Account temporaer gesperrt. Versuchen Sie es in 15 Minuten erneut.', + ); + } + } + + // 2FA pruefen + if (user.twoFactorEnabled) { + if (!dto.totpCode) { + return { + accessToken: '', + user: { + id: user.id, + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + role: user.role, + }, + requiresTwoFactor: true, + }; + } + + const totpValid = this.totp.verify( + dto.totpCode, + localAuth.totpSecret ?? '', + ); + if (!totpValid) { + throw new UnauthorizedException('Ungueltiger 2FA-Code'); + } + } + + // Erfolgreicher Login: Counter zuruecksetzen + await this.prisma.user.update({ + where: { id: user.id }, + data: { + failedLoginAttempts: 0, + lastLogin: new Date(), + }, + }); + + // Tenant-Info + const primaryMembership = user.tenantMemberships[0]; + + // Tokens generieren + const tokens = await this.generateTokenPair({ + sub: user.id, + email: user.email, + role: user.role, + tenantId: primaryMembership?.tenant.id, + tenantSlug: primaryMembership?.tenant.slug, + }); + + this.logger.log(`Login erfolgreich: ${user.email}`); + + return { + accessToken: tokens.accessToken, + user: { + id: user.id, + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + role: user.role, + }, + }; + } + + /** + * Refresh-Token gegen neues Token-Paar tauschen. + */ + async refreshTokens(refreshToken: string): Promise { + try { + const payload = this.jwt.verify(refreshToken); + + // Refresh-Token-Familie pruefen (Token-Rotation) + const isValid = await this.redis.isRefreshTokenFamilyValid( + payload.sub, + payload.jti, + ); + if (!isValid) { + // Moeglicherweise Refresh-Token-Diebstahl: alle invalidieren + this.logger.warn( + `Verdaechtiger Refresh-Token Wiederverwendung fuer User ${payload.sub}`, + ); + await this.redis.invalidateAllRefreshTokens(payload.sub); + throw new UnauthorizedException('Refresh Token ungueltig'); + } + + // Alten Refresh-Token invalidieren + await this.redis.blockToken(payload.jti, 7 * 24 * 60 * 60); + + // Neue Tokens generieren + return this.generateTokenPair({ + sub: payload.sub, + email: payload.email, + role: payload.role, + tenantId: payload.tenantId, + tenantSlug: payload.tenantSlug, + }); + } catch (error) { + if (error instanceof UnauthorizedException) throw error; + throw new UnauthorizedException('Refresh Token ungueltig oder abgelaufen'); + } + } + + /** + * Logout: Access- und Refresh-Token invalidieren. + */ + async logout(accessToken: JwtPayload, refreshToken?: string): Promise { + // Access-Token blocken (Restlaufzeit) + const ttl = accessToken.exp - Math.floor(Date.now() / 1000); + if (ttl > 0) { + await this.redis.blockToken(accessToken.jti, ttl); + } + + // Refresh-Token blocken + if (refreshToken) { + try { + const refreshPayload = this.jwt.verify(refreshToken); + const refreshTtl = + refreshPayload.exp - Math.floor(Date.now() / 1000); + if (refreshTtl > 0) { + await this.redis.blockToken(refreshPayload.jti, refreshTtl); + } + } catch { + // Refresh-Token ist bereits abgelaufen - ignorieren + } + } + + this.logger.log(`Logout: User ${accessToken.sub}`); + } + + /** + * Token-Paar generieren (Access + Refresh). + */ + private async generateTokenPair( + payload: Omit, + ): Promise { + const accessJti = uuidv4(); + const refreshJti = uuidv4(); + + const accessToken = this.jwt.sign({ + ...payload, + jti: accessJti, + }); + + const refreshExpiry = this.config.get( + 'JWT_REFRESH_TOKEN_EXPIRY', + '7d', + ); + const refreshToken = this.jwt.sign( + { + ...payload, + jti: refreshJti, + }, + { expiresIn: refreshExpiry }, + ); + + // Refresh-Token-Familie in Redis registrieren + await this.redis.setRefreshTokenFamily( + payload.sub, + refreshJti, + 7 * 24 * 60 * 60, // 7 Tage + ); + + return { accessToken, refreshToken }; + } +} diff --git a/packages/core-service/src/core/auth/dto/login.dto.ts b/packages/core-service/src/core/auth/dto/login.dto.ts new file mode 100644 index 0000000..bceaba4 --- /dev/null +++ b/packages/core-service/src/core/auth/dto/login.dto.ts @@ -0,0 +1,30 @@ +import { IsEmail, IsNotEmpty, IsOptional, IsString, MinLength } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class LoginDto { + @ApiProperty({ + example: 'admin@xinion.de', + description: 'E-Mail-Adresse des Benutzers', + }) + @IsEmail({}, { message: 'Bitte gueltige E-Mail-Adresse angeben' }) + @IsNotEmpty({ message: 'E-Mail darf nicht leer sein' }) + email!: string; + + @ApiProperty({ + example: 'SicheresPasswort123!', + description: 'Passwort (mindestens 8 Zeichen)', + }) + @IsString() + @IsNotEmpty({ message: 'Passwort darf nicht leer sein' }) + @MinLength(8, { message: 'Passwort muss mindestens 8 Zeichen lang sein' }) + password!: string; + + @ApiProperty({ + example: '123456', + description: 'TOTP 2FA-Code (nur wenn 2FA aktiviert)', + required: false, + }) + @IsOptional() + @IsString() + totpCode?: string; +} diff --git a/packages/core-service/src/core/auth/strategies/jwt.strategy.ts b/packages/core-service/src/core/auth/strategies/jwt.strategy.ts new file mode 100644 index 0000000..97d133b --- /dev/null +++ b/packages/core-service/src/core/auth/strategies/jwt.strategy.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import * as fs from 'fs'; +import { JwtPayload } from '../../../common/decorators/current-user.decorator'; + +/** + * JwtStrategy - Passport-Strategy fuer RS256 JWT-Validierung. + * + * Extrahiert den Token aus dem Authorization-Header (Bearer Token). + * Validiert Signatur (RS256), Issuer und Expiration automatisch. + */ +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor(config: ConfigService) { + const publicKeyPath = config.get( + 'JWT_PUBLIC_KEY_PATH', + '/app/keys/jwt-public.pem', + ); + + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: fs.readFileSync(publicKeyPath, 'utf8'), + algorithms: ['RS256'], + issuer: config.get('JWT_ISSUER', 'insight-platform'), + }); + } + + /** + * Wird nach erfolgreicher JWT-Validierung aufgerufen. + * Der Return-Wert landet in request.user. + */ + validate(payload: JwtPayload): JwtPayload { + return { + sub: payload.sub, + email: payload.email, + role: payload.role, + tenantId: payload.tenantId, + tenantSlug: payload.tenantSlug, + jti: payload.jti, + iat: payload.iat, + exp: payload.exp, + }; + } +} diff --git a/packages/core-service/src/core/auth/totp.service.ts b/packages/core-service/src/core/auth/totp.service.ts new file mode 100644 index 0000000..d43cb47 --- /dev/null +++ b/packages/core-service/src/core/auth/totp.service.ts @@ -0,0 +1,54 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { authenticator } from 'otplib'; +import * as QRCode from 'qrcode'; + +/** + * TotpService - TOTP 2FA (Time-based One-Time Password). + * + * Verwendet den Google Authenticator kompatiblen TOTP-Algorithmus. + * Secrets werden verschluesselt in der Datenbank gespeichert. + */ +@Injectable() +export class TotpService { + private readonly logger = new Logger(TotpService.name); + + constructor() { + // TOTP Konfiguration + authenticator.options = { + step: 30, // 30 Sekunden + window: 1, // +/- 1 Schritt Toleranz + digits: 6, + }; + } + + /** + * Neues TOTP-Secret generieren. + */ + generateSecret(): string { + return authenticator.generateSecret(); + } + + /** + * QR-Code als Data-URL generieren (fuer Authenticator-App Setup). + */ + async generateQrCode(email: string, secret: string): Promise { + const otpauthUrl = authenticator.keyuri( + email, + 'INSIGHT Platform', + secret, + ); + return QRCode.toDataURL(otpauthUrl); + } + + /** + * TOTP-Code verifizieren. + */ + verify(token: string, secret: string): boolean { + try { + return authenticator.verify({ token, secret }); + } catch { + this.logger.warn('TOTP-Verifizierung fehlgeschlagen'); + return false; + } + } +} diff --git a/packages/core-service/src/core/modules/.gitkeep b/packages/core-service/src/core/modules/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/packages/core-service/src/core/tenants/.gitkeep b/packages/core-service/src/core/tenants/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/packages/core-service/src/core/tenants/dto/add-member.dto.ts b/packages/core-service/src/core/tenants/dto/add-member.dto.ts new file mode 100644 index 0000000..dc0f046 --- /dev/null +++ b/packages/core-service/src/core/tenants/dto/add-member.dto.ts @@ -0,0 +1,19 @@ +import { IsNotEmpty, IsOptional, IsString, IsUUID, IsIn } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class AddMemberDto { + @ApiProperty({ example: '550e8400-e29b-41d4-a716-446655440000' }) + @IsUUID() + @IsNotEmpty() + userId!: string; + + @ApiProperty({ + example: 'MEMBER', + enum: ['ADMIN', 'MEMBER', 'VIEWER'], + required: false, + }) + @IsOptional() + @IsString() + @IsIn(['ADMIN', 'MEMBER', 'VIEWER']) + role?: string; +} diff --git a/packages/core-service/src/core/tenants/dto/create-tenant.dto.ts b/packages/core-service/src/core/tenants/dto/create-tenant.dto.ts new file mode 100644 index 0000000..3eeea69 --- /dev/null +++ b/packages/core-service/src/core/tenants/dto/create-tenant.dto.ts @@ -0,0 +1,37 @@ +import { + IsNotEmpty, + IsOptional, + IsString, + Matches, + MaxLength, + IsObject, +} from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateTenantDto { + @ApiProperty({ example: 'ACME Corporation' }) + @IsString() + @IsNotEmpty() + @MaxLength(200) + name!: string; + + @ApiProperty({ + example: 'acme-corp', + description: 'URL-freundlicher Kurzname (a-z, 0-9, Bindestrich)', + }) + @IsString() + @IsNotEmpty() + @MaxLength(50) + @Matches(/^[a-z0-9][a-z0-9-]*[a-z0-9]$/, { + message: 'Slug: nur Kleinbuchstaben, Zahlen und Bindestriche erlaubt', + }) + slug!: string; + + @ApiProperty({ + example: { locale: 'de-DE', timezone: 'Europe/Berlin' }, + required: false, + }) + @IsOptional() + @IsObject() + settings?: Record; +} diff --git a/packages/core-service/src/core/tenants/dto/update-tenant.dto.ts b/packages/core-service/src/core/tenants/dto/update-tenant.dto.ts new file mode 100644 index 0000000..bf86633 --- /dev/null +++ b/packages/core-service/src/core/tenants/dto/update-tenant.dto.ts @@ -0,0 +1,29 @@ +import { + IsBoolean, + IsObject, + IsOptional, + IsString, + MaxLength, +} from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class UpdateTenantDto { + @ApiProperty({ example: 'ACME Corp. GmbH', required: false }) + @IsOptional() + @IsString() + @MaxLength(200) + name?: string; + + @ApiProperty({ example: true, required: false }) + @IsOptional() + @IsBoolean() + isActive?: boolean; + + @ApiProperty({ + example: { locale: 'de-DE', timezone: 'Europe/Berlin' }, + required: false, + }) + @IsOptional() + @IsObject() + settings?: Record; +} diff --git a/packages/core-service/src/core/tenants/tenants.controller.ts b/packages/core-service/src/core/tenants/tenants.controller.ts new file mode 100644 index 0000000..bb77761 --- /dev/null +++ b/packages/core-service/src/core/tenants/tenants.controller.ts @@ -0,0 +1,110 @@ +import { + Controller, + Get, + Post, + Patch, + Delete, + Body, + Param, + Query, + UseGuards, + ParseUUIDPipe, +} from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; +import { TenantsService } from './tenants.service'; +import { CreateTenantDto } from './dto/create-tenant.dto'; +import { UpdateTenantDto } from './dto/update-tenant.dto'; +import { AddMemberDto } from './dto/add-member.dto'; +import { Roles } from '../../common/decorators/roles.decorator'; +import { RolesGuard } from '../../common/guards/roles.guard'; + +@ApiTags('Mandanten') +@ApiBearerAuth('access-token') +@Controller('tenants') +export class TenantsController { + constructor(private readonly tenantsService: TenantsService) {} + + /** + * GET /api/v1/tenants + * Alle Mandanten auflisten (nur PLATFORM_ADMIN). + */ + @Get() + @Roles('PLATFORM_ADMIN') + @UseGuards(RolesGuard) + @ApiOperation({ summary: 'Alle Mandanten auflisten (Admin)' }) + async findAll( + @Query('page') page?: number, + @Query('limit') limit?: number, + ) { + return this.tenantsService.findAll(page ?? 1, limit ?? 20); + } + + /** + * GET /api/v1/tenants/:id + * Mandant nach ID (nur PLATFORM_ADMIN). + */ + @Get(':id') + @Roles('PLATFORM_ADMIN') + @UseGuards(RolesGuard) + @ApiOperation({ summary: 'Mandant nach ID abrufen (Admin)' }) + async findById(@Param('id', ParseUUIDPipe) id: string) { + return this.tenantsService.findById(id); + } + + /** + * POST /api/v1/tenants + * Neuen Mandant anlegen (nur PLATFORM_ADMIN). + */ + @Post() + @Roles('PLATFORM_ADMIN') + @UseGuards(RolesGuard) + @ApiOperation({ summary: 'Neuen Mandanten anlegen (Admin)' }) + async create(@Body() dto: CreateTenantDto) { + return this.tenantsService.create(dto); + } + + /** + * PATCH /api/v1/tenants/:id + * Mandant aktualisieren (nur PLATFORM_ADMIN). + */ + @Patch(':id') + @Roles('PLATFORM_ADMIN') + @UseGuards(RolesGuard) + @ApiOperation({ summary: 'Mandant aktualisieren (Admin)' }) + async update( + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: UpdateTenantDto, + ) { + return this.tenantsService.update(id, dto); + } + + /** + * POST /api/v1/tenants/:id/members + * User einem Mandant zuweisen (nur PLATFORM_ADMIN). + */ + @Post(':id/members') + @Roles('PLATFORM_ADMIN') + @UseGuards(RolesGuard) + @ApiOperation({ summary: 'Benutzer zu Mandant hinzufuegen (Admin)' }) + async addMember( + @Param('id', ParseUUIDPipe) tenantId: string, + @Body() dto: AddMemberDto, + ) { + return this.tenantsService.addMember(tenantId, dto.userId, dto.role); + } + + /** + * DELETE /api/v1/tenants/:id/members/:userId + * User aus Mandant entfernen (nur PLATFORM_ADMIN). + */ + @Delete(':id/members/:userId') + @Roles('PLATFORM_ADMIN') + @UseGuards(RolesGuard) + @ApiOperation({ summary: 'Benutzer aus Mandant entfernen (Admin)' }) + async removeMember( + @Param('id', ParseUUIDPipe) tenantId: string, + @Param('userId', ParseUUIDPipe) userId: string, + ) { + return this.tenantsService.removeMember(tenantId, userId); + } +} diff --git a/packages/core-service/src/core/tenants/tenants.module.ts b/packages/core-service/src/core/tenants/tenants.module.ts new file mode 100644 index 0000000..522f740 --- /dev/null +++ b/packages/core-service/src/core/tenants/tenants.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { TenantsController } from './tenants.controller'; +import { TenantsService } from './tenants.service'; + +@Module({ + controllers: [TenantsController], + providers: [TenantsService], + exports: [TenantsService], +}) +export class TenantsModule {} diff --git a/packages/core-service/src/core/tenants/tenants.service.ts b/packages/core-service/src/core/tenants/tenants.service.ts new file mode 100644 index 0000000..9dff481 --- /dev/null +++ b/packages/core-service/src/core/tenants/tenants.service.ts @@ -0,0 +1,166 @@ +import { + Injectable, + ConflictException, + NotFoundException, + Logger, +} from '@nestjs/common'; +import { PrismaService } from '../../prisma/prisma.service'; +import { CreateTenantDto } from './dto/create-tenant.dto'; +import { UpdateTenantDto } from './dto/update-tenant.dto'; + +@Injectable() +export class TenantsService { + private readonly logger = new Logger(TenantsService.name); + + constructor(private readonly prisma: PrismaService) {} + + /** + * Neuen Tenant (Mandant) anlegen. + */ + async create(dto: CreateTenantDto) { + // Slug-Duplikat pruefen + const existing = await this.prisma.tenant.findUnique({ + where: { slug: dto.slug }, + }); + if (existing) { + throw new ConflictException(`Tenant-Slug "${dto.slug}" bereits vergeben`); + } + + const tenant = await this.prisma.tenant.create({ + data: { + name: dto.name, + slug: dto.slug, + isActive: true, + settings: dto.settings ?? {}, + }, + }); + + this.logger.log(`Tenant erstellt: ${tenant.name} (${tenant.slug})`); + + // TODO: Tenant-Datenbank erstellen (tenant_{slug}) + // Das wird spaeter per Prisma Migrate automatisiert + + return tenant; + } + + /** + * Tenant nach ID finden. + */ + async findById(id: string) { + const tenant = await this.prisma.tenant.findUnique({ + where: { id }, + include: { + _count: { + select: { members: true }, + }, + }, + }); + + if (!tenant) { + throw new NotFoundException('Mandant nicht gefunden'); + } + + return { + ...tenant, + memberCount: tenant._count.members, + }; + } + + /** + * Alle Tenants auflisten. + */ + async findAll(page = 1, limit = 20) { + const skip = (page - 1) * limit; + + const [tenants, total] = await Promise.all([ + this.prisma.tenant.findMany({ + skip, + take: limit, + orderBy: { createdAt: 'desc' }, + include: { + _count: { + select: { members: true }, + }, + }, + }), + this.prisma.tenant.count(), + ]); + + return { + data: tenants.map((t) => ({ + id: t.id, + name: t.name, + slug: t.slug, + isActive: t.isActive, + memberCount: t._count.members, + createdAt: t.createdAt, + })), + meta: { + total, + page, + limit, + totalPages: Math.ceil(total / limit), + }, + }; + } + + /** + * Tenant aktualisieren. + */ + async update(id: string, dto: UpdateTenantDto) { + const tenant = await this.prisma.tenant.findUnique({ where: { id } }); + if (!tenant) { + throw new NotFoundException('Mandant nicht gefunden'); + } + + return this.prisma.tenant.update({ + where: { id }, + data: { + name: dto.name, + isActive: dto.isActive, + settings: dto.settings, + }, + }); + } + + /** + * User einem Tenant zuweisen. + */ + async addMember(tenantId: string, userId: string, role = 'MEMBER') { + // Pruefen ob bereits Mitglied + const existing = await this.prisma.tenantMembership.findUnique({ + where: { + userId_tenantId: { userId, tenantId }, + }, + }); + + if (existing) { + throw new ConflictException('Benutzer ist bereits Mitglied'); + } + + return this.prisma.tenantMembership.create({ + data: { + userId, + tenantId, + tenantRole: role, + isActive: true, + }, + include: { + user: { select: { email: true, firstName: true, lastName: true } }, + tenant: { select: { name: true, slug: true } }, + }, + }); + } + + /** + * User aus Tenant entfernen (Soft-Delete). + */ + async removeMember(tenantId: string, userId: string) { + return this.prisma.tenantMembership.update({ + where: { + userId_tenantId: { userId, tenantId }, + }, + data: { isActive: false }, + }); + } +} diff --git a/packages/core-service/src/core/users/.gitkeep b/packages/core-service/src/core/users/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/packages/core-service/src/core/users/dto/create-user.dto.ts b/packages/core-service/src/core/users/dto/create-user.dto.ts new file mode 100644 index 0000000..f355ec0 --- /dev/null +++ b/packages/core-service/src/core/users/dto/create-user.dto.ts @@ -0,0 +1,46 @@ +import { + IsEmail, + IsNotEmpty, + IsOptional, + IsString, + MinLength, + MaxLength, + IsIn, +} from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateUserDto { + @ApiProperty({ example: 'max.mustermann@xinion.de' }) + @IsEmail({}, { message: 'Bitte gueltige E-Mail-Adresse angeben' }) + @IsNotEmpty() + email!: string; + + @ApiProperty({ example: 'SicheresPasswort123!' }) + @IsString() + @IsNotEmpty() + @MinLength(8, { message: 'Passwort muss mindestens 8 Zeichen lang sein' }) + @MaxLength(128, { message: 'Passwort darf maximal 128 Zeichen lang sein' }) + password!: string; + + @ApiProperty({ example: 'Max' }) + @IsString() + @IsNotEmpty() + @MaxLength(100) + firstName!: string; + + @ApiProperty({ example: 'Mustermann' }) + @IsString() + @IsNotEmpty() + @MaxLength(100) + lastName!: string; + + @ApiProperty({ + example: 'USER', + enum: ['PLATFORM_ADMIN', 'TENANT_ADMIN', 'USER'], + required: false, + }) + @IsOptional() + @IsString() + @IsIn(['PLATFORM_ADMIN', 'TENANT_ADMIN', 'USER']) + role?: string; +} diff --git a/packages/core-service/src/core/users/dto/update-user.dto.ts b/packages/core-service/src/core/users/dto/update-user.dto.ts new file mode 100644 index 0000000..d73f6b6 --- /dev/null +++ b/packages/core-service/src/core/users/dto/update-user.dto.ts @@ -0,0 +1,21 @@ +import { IsBoolean, IsOptional, IsString, MaxLength } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class UpdateUserDto { + @ApiProperty({ example: 'Max', required: false }) + @IsOptional() + @IsString() + @MaxLength(100) + firstName?: string; + + @ApiProperty({ example: 'Mustermann', required: false }) + @IsOptional() + @IsString() + @MaxLength(100) + lastName?: string; + + @ApiProperty({ example: true, required: false }) + @IsOptional() + @IsBoolean() + isActive?: boolean; +} diff --git a/packages/core-service/src/core/users/users.controller.ts b/packages/core-service/src/core/users/users.controller.ts new file mode 100644 index 0000000..83b1da8 --- /dev/null +++ b/packages/core-service/src/core/users/users.controller.ts @@ -0,0 +1,89 @@ +import { + Controller, + Get, + Post, + Patch, + Body, + Param, + Query, + UseGuards, + ParseUUIDPipe, +} from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; +import { UsersService } from './users.service'; +import { CreateUserDto } from './dto/create-user.dto'; +import { UpdateUserDto } from './dto/update-user.dto'; +import { Roles } from '../../common/decorators/roles.decorator'; +import { CurrentUser, JwtPayload } from '../../common/decorators/current-user.decorator'; +import { RolesGuard } from '../../common/guards/roles.guard'; + +@ApiTags('Benutzer') +@ApiBearerAuth('access-token') +@Controller('users') +export class UsersController { + constructor(private readonly usersService: UsersService) {} + + /** + * GET /api/v1/users/me + * Eigenes Profil abrufen. + */ + @Get('me') + @ApiOperation({ summary: 'Eigenes Profil abrufen' }) + async getProfile(@CurrentUser('sub') userId: string) { + return this.usersService.findById(userId); + } + + /** + * GET /api/v1/users + * Alle User auflisten (nur PLATFORM_ADMIN). + */ + @Get() + @Roles('PLATFORM_ADMIN') + @UseGuards(RolesGuard) + @ApiOperation({ summary: 'Alle Benutzer auflisten (Admin)' }) + async findAll( + @Query('page') page?: number, + @Query('limit') limit?: number, + ) { + return this.usersService.findAll(page ?? 1, limit ?? 20); + } + + /** + * GET /api/v1/users/:id + * User nach ID (nur PLATFORM_ADMIN). + */ + @Get(':id') + @Roles('PLATFORM_ADMIN') + @UseGuards(RolesGuard) + @ApiOperation({ summary: 'Benutzer nach ID abrufen (Admin)' }) + async findById(@Param('id', ParseUUIDPipe) id: string) { + return this.usersService.findById(id); + } + + /** + * POST /api/v1/users + * Neuen User anlegen (nur PLATFORM_ADMIN). + */ + @Post() + @Roles('PLATFORM_ADMIN') + @UseGuards(RolesGuard) + @ApiOperation({ summary: 'Neuen Benutzer anlegen (Admin)' }) + async create(@Body() dto: CreateUserDto) { + return this.usersService.create(dto); + } + + /** + * PATCH /api/v1/users/:id + * User aktualisieren (nur PLATFORM_ADMIN). + */ + @Patch(':id') + @Roles('PLATFORM_ADMIN') + @UseGuards(RolesGuard) + @ApiOperation({ summary: 'Benutzer aktualisieren (Admin)' }) + async update( + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: UpdateUserDto, + ) { + return this.usersService.update(id, dto); + } +} diff --git a/packages/core-service/src/core/users/users.module.ts b/packages/core-service/src/core/users/users.module.ts new file mode 100644 index 0000000..513776d --- /dev/null +++ b/packages/core-service/src/core/users/users.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { UsersController } from './users.controller'; +import { UsersService } from './users.service'; + +@Module({ + controllers: [UsersController], + providers: [UsersService], + exports: [UsersService], +}) +export class UsersModule {} diff --git a/packages/core-service/src/core/users/users.service.ts b/packages/core-service/src/core/users/users.service.ts new file mode 100644 index 0000000..920ec3e --- /dev/null +++ b/packages/core-service/src/core/users/users.service.ts @@ -0,0 +1,174 @@ +import { + Injectable, + ConflictException, + NotFoundException, + Logger, +} from '@nestjs/common'; +import * as bcrypt from 'bcrypt'; +import { ConfigService } from '@nestjs/config'; +import { PrismaService } from '../../prisma/prisma.service'; +import { CreateUserDto } from './dto/create-user.dto'; +import { UpdateUserDto } from './dto/update-user.dto'; + +@Injectable() +export class UsersService { + private readonly logger = new Logger(UsersService.name); + + constructor( + private readonly prisma: PrismaService, + private readonly config: ConfigService, + ) {} + + /** + * Neuen User anlegen (mit lokalem Auth-Provider). + */ + async create(dto: CreateUserDto) { + // Email-Duplikat pruefen + const existing = await this.prisma.user.findUnique({ + where: { email: dto.email.toLowerCase() }, + }); + if (existing) { + throw new ConflictException('E-Mail-Adresse bereits vergeben'); + } + + // Passwort hashen (Bcrypt Cost 12) + const bcryptCost = this.config.get('BCRYPT_COST', 12); + const passwordHash = await bcrypt.hash(dto.password, bcryptCost); + + // User + AuthProvider in einer Transaktion anlegen + const user = await this.prisma.user.create({ + data: { + email: dto.email.toLowerCase(), + firstName: dto.firstName, + lastName: dto.lastName, + role: dto.role ?? 'USER', + isActive: true, + authProvider: { + create: { + provider: 'LOCAL', + passwordHash, + }, + }, + }, + include: { + authProvider: { + select: { provider: true, createdAt: true }, + }, + }, + }); + + this.logger.log(`User erstellt: ${user.email} (${user.role})`); + + // Passwort-Hash nicht zurueckgeben + return { + id: user.id, + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + role: user.role, + isActive: user.isActive, + createdAt: user.createdAt, + }; + } + + /** + * User nach ID finden. + */ + async findById(id: string) { + const user = await this.prisma.user.findUnique({ + where: { id }, + include: { + tenantMemberships: { + include: { tenant: { select: { id: true, name: true, slug: true } } }, + where: { isActive: true }, + }, + }, + }); + + if (!user) { + throw new NotFoundException('Benutzer nicht gefunden'); + } + + return { + id: user.id, + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + role: user.role, + isActive: user.isActive, + twoFactorEnabled: user.twoFactorEnabled, + tenants: user.tenantMemberships.map((m) => ({ + id: m.tenant.id, + name: m.tenant.name, + slug: m.tenant.slug, + role: m.tenantRole, + })), + lastLogin: user.lastLogin, + createdAt: user.createdAt, + }; + } + + /** + * User aktualisieren. + */ + async update(id: string, dto: UpdateUserDto) { + const user = await this.prisma.user.findUnique({ where: { id } }); + if (!user) { + throw new NotFoundException('Benutzer nicht gefunden'); + } + + const updated = await this.prisma.user.update({ + where: { id }, + data: { + firstName: dto.firstName, + lastName: dto.lastName, + isActive: dto.isActive, + }, + }); + + return { + id: updated.id, + email: updated.email, + firstName: updated.firstName, + lastName: updated.lastName, + role: updated.role, + isActive: updated.isActive, + }; + } + + /** + * Alle User auflisten (fuer Admin). + */ + async findAll(page = 1, limit = 20) { + const skip = (page - 1) * limit; + + const [users, total] = await Promise.all([ + this.prisma.user.findMany({ + skip, + take: limit, + orderBy: { createdAt: 'desc' }, + select: { + id: true, + email: true, + firstName: true, + lastName: true, + role: true, + isActive: true, + lastLogin: true, + createdAt: true, + }, + }), + this.prisma.user.count(), + ]); + + return { + data: users, + meta: { + total, + page, + limit, + totalPages: Math.ceil(total / limit), + }, + }; + } +} diff --git a/packages/core-service/src/health/health.controller.ts b/packages/core-service/src/health/health.controller.ts new file mode 100644 index 0000000..4979fa8 --- /dev/null +++ b/packages/core-service/src/health/health.controller.ts @@ -0,0 +1,72 @@ +import { Controller, Get } from '@nestjs/common'; +import { ApiTags, ApiOperation } from '@nestjs/swagger'; +import { Public } from '../common/decorators/public.decorator'; +import { PrismaService } from '../prisma/prisma.service'; +import { RedisService } from '../redis/redis.service'; + +interface HealthResponse { + status: 'ok' | 'error'; + timestamp: string; + version: string; + services: { + database: 'up' | 'down'; + redis: 'up' | 'down'; + }; +} + +@ApiTags('Health') +@Controller('health') +export class HealthController { + constructor( + private readonly prisma: PrismaService, + private readonly redis: RedisService, + ) {} + + @Get() + @Public() + @ApiOperation({ summary: 'Health-Check fuer alle Services' }) + async check(): Promise { + const [dbStatus, redisStatus] = await Promise.allSettled([ + this.checkDatabase(), + this.checkRedis(), + ]); + + const allUp = + dbStatus.status === 'fulfilled' && + dbStatus.value && + redisStatus.status === 'fulfilled' && + redisStatus.value; + + return { + status: allUp ? 'ok' : 'error', + timestamp: new Date().toISOString(), + version: '0.1.0', + services: { + database: + dbStatus.status === 'fulfilled' && dbStatus.value ? 'up' : 'down', + redis: + redisStatus.status === 'fulfilled' && redisStatus.value + ? 'up' + : 'down', + }, + }; + } + + private async checkDatabase(): Promise { + try { + await this.prisma.$queryRaw`SELECT 1`; + return true; + } catch { + return false; + } + } + + private async checkRedis(): Promise { + try { + const pong = await this.redis.ping(); + return pong === 'PONG'; + } catch { + return false; + } + } +} diff --git a/packages/core-service/src/health/health.module.ts b/packages/core-service/src/health/health.module.ts new file mode 100644 index 0000000..181df98 --- /dev/null +++ b/packages/core-service/src/health/health.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { HealthController } from './health.controller'; +import { PrismaModule } from '../prisma/prisma.module'; +import { RedisModule } from '../redis/redis.module'; + +@Module({ + imports: [PrismaModule, RedisModule], + controllers: [HealthController], +}) +export class HealthModule {} diff --git a/packages/core-service/src/main.ts b/packages/core-service/src/main.ts new file mode 100644 index 0000000..3f2a509 --- /dev/null +++ b/packages/core-service/src/main.ts @@ -0,0 +1,82 @@ +import { NestFactory } from '@nestjs/core'; +import { ValidationPipe, Logger } from '@nestjs/common'; +import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; +import * as cookieParser from 'cookie-parser'; +import helmet from 'helmet'; +import { AppModule } from './app.module'; + +async function bootstrap(): Promise { + const logger = new Logger('Bootstrap'); + const app = await NestFactory.create(AppModule, { + logger: ['error', 'warn', 'log', 'debug', 'verbose'], + }); + + // Security + app.use(helmet()); + app.use(cookieParser()); + + // CORS + const corsOrigins = process.env.CORS_ORIGINS?.split(',') ?? [ + 'https://insight-dev.xinion.lan', + ]; + app.enableCors({ + origin: corsOrigins, + credentials: true, + methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], + allowedHeaders: [ + 'Content-Type', + 'Authorization', + 'X-Tenant-ID', + 'X-Request-ID', + ], + }); + + // Global Validation Pipe (Sicherheitsregel: whitelist + forbidNonWhitelisted) + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + forbidNonWhitelisted: true, + transform: true, + transformOptions: { enableImplicitConversion: true }, + }), + ); + + // Global Prefix + app.setGlobalPrefix('api/v1', { + exclude: ['health'], + }); + + // Swagger (nur Development) + if (process.env.NODE_ENV !== 'production') { + const config = new DocumentBuilder() + .setTitle('INSIGHT Platform API') + .setDescription('Multi-Tenant Business Platform API') + .setVersion('0.1.0') + .addBearerAuth( + { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + description: 'Access Token (RS256)', + }, + 'access-token', + ) + .addCookieAuth('refresh_token', { + type: 'apiKey', + in: 'cookie', + description: 'HttpOnly Refresh Token', + }) + .build(); + + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('api/docs', app, document); + logger.log('Swagger UI: /api/docs'); + } + + const port = process.env.APP_PORT ?? 3000; + await app.listen(port); + logger.log(`Core-Service laeuft auf Port ${port}`); + logger.log(`Umgebung: ${process.env.NODE_ENV ?? 'development'}`); +} + +bootstrap(); diff --git a/packages/core-service/src/prisma/.gitkeep b/packages/core-service/src/prisma/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/packages/core-service/src/prisma/prisma.module.ts b/packages/core-service/src/prisma/prisma.module.ts new file mode 100644 index 0000000..5321f9f --- /dev/null +++ b/packages/core-service/src/prisma/prisma.module.ts @@ -0,0 +1,10 @@ +import { Global, Module } from '@nestjs/common'; +import { PrismaService } from './prisma.service'; +import { TenantPrismaService } from './tenant-prisma.service'; + +@Global() +@Module({ + providers: [PrismaService, TenantPrismaService], + exports: [PrismaService, TenantPrismaService], +}) +export class PrismaModule {} diff --git a/packages/core-service/src/prisma/prisma.service.ts b/packages/core-service/src/prisma/prisma.service.ts new file mode 100644 index 0000000..48da17f --- /dev/null +++ b/packages/core-service/src/prisma/prisma.service.ts @@ -0,0 +1,32 @@ +import { Injectable, OnModuleInit, OnModuleDestroy, Logger } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; + +@Injectable() +export class PrismaService + extends PrismaClient + implements OnModuleInit, OnModuleDestroy +{ + private readonly logger = new Logger(PrismaService.name); + + constructor() { + super({ + log: [ + { emit: 'event', level: 'query' }, + { emit: 'stdout', level: 'info' }, + { emit: 'stdout', level: 'warn' }, + { emit: 'stdout', level: 'error' }, + ], + }); + } + + async onModuleInit(): Promise { + this.logger.log('Verbinde mit PostgreSQL (platform_core)...'); + await this.$connect(); + this.logger.log('PostgreSQL Verbindung hergestellt.'); + } + + async onModuleDestroy(): Promise { + this.logger.log('Trenne PostgreSQL Verbindung...'); + await this.$disconnect(); + } +} diff --git a/packages/core-service/src/prisma/tenant-prisma.service.ts b/packages/core-service/src/prisma/tenant-prisma.service.ts new file mode 100644 index 0000000..693e826 --- /dev/null +++ b/packages/core-service/src/prisma/tenant-prisma.service.ts @@ -0,0 +1,103 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; + +/** + * TenantPrismaService - Verwaltet dynamische Datenbankverbindungen pro Mandant. + * + * Jeder Tenant hat eine eigene Datenbank (tenant_{slug}). + * Verbindungen werden gecacht und bei Inaktivitaet automatisch geschlossen. + */ +@Injectable() +export class TenantPrismaService { + private readonly logger = new Logger(TenantPrismaService.name); + private readonly clients = new Map< + string, + { client: PrismaClient; lastUsed: number } + >(); + + // Maximale Inaktivitaetszeit in Millisekunden (30 Minuten) + private readonly MAX_IDLE_TIME = 30 * 60 * 1000; + + /** + * Gibt einen PrismaClient fuer den angegebenen Tenant zurueck. + * Erstellt eine neue Verbindung oder nutzt eine gecachte. + */ + async getClient(tenantSlug: string): Promise { + const existing = this.clients.get(tenantSlug); + if (existing) { + existing.lastUsed = Date.now(); + return existing.client; + } + + const dbName = `tenant_${tenantSlug}`; + const baseUrl = process.env.DATABASE_URL_DIRECT ?? process.env.DATABASE_URL; + if (!baseUrl) { + throw new Error('DATABASE_URL ist nicht konfiguriert'); + } + + // URL modifizieren: Datenbankname ersetzen + const url = new URL(baseUrl); + url.pathname = `/${dbName}`; + + const client = new PrismaClient({ + datasources: { + db: { url: url.toString() }, + }, + }); + + await client.$connect(); + this.logger.log(`Tenant-DB Verbindung hergestellt: ${dbName}`); + + this.clients.set(tenantSlug, { + client, + lastUsed: Date.now(), + }); + + return client; + } + + /** + * Schliesst eine bestimmte Tenant-Verbindung. + */ + async disconnectTenant(tenantSlug: string): Promise { + const existing = this.clients.get(tenantSlug); + if (existing) { + await existing.client.$disconnect(); + this.clients.delete(tenantSlug); + this.logger.log(`Tenant-DB Verbindung geschlossen: tenant_${tenantSlug}`); + } + } + + /** + * Schliesst alle inaktiven Verbindungen. + * Wird periodisch vom CleanupService aufgerufen. + */ + async cleanupIdleConnections(): Promise { + const now = Date.now(); + let closed = 0; + + for (const [slug, entry] of this.clients.entries()) { + if (now - entry.lastUsed > this.MAX_IDLE_TIME) { + await entry.client.$disconnect(); + this.clients.delete(slug); + this.logger.log( + `Idle Tenant-DB Verbindung geschlossen: tenant_${slug}`, + ); + closed++; + } + } + + return closed; + } + + /** + * Schliesst alle Tenant-Verbindungen (Shutdown). + */ + async disconnectAll(): Promise { + for (const [slug, entry] of this.clients.entries()) { + await entry.client.$disconnect(); + this.logger.log(`Tenant-DB Verbindung geschlossen: tenant_${slug}`); + } + this.clients.clear(); + } +} diff --git a/packages/core-service/src/redis/redis.module.ts b/packages/core-service/src/redis/redis.module.ts new file mode 100644 index 0000000..b9cfabf --- /dev/null +++ b/packages/core-service/src/redis/redis.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common'; +import { RedisService } from './redis.service'; + +@Global() +@Module({ + providers: [RedisService], + exports: [RedisService], +}) +export class RedisModule {} diff --git a/packages/core-service/src/redis/redis.service.ts b/packages/core-service/src/redis/redis.service.ts new file mode 100644 index 0000000..eed4bb9 --- /dev/null +++ b/packages/core-service/src/redis/redis.service.ts @@ -0,0 +1,144 @@ +import { + Injectable, + OnModuleInit, + OnModuleDestroy, + Logger, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import Redis from 'ioredis'; + +/** + * RedisService - Zentraler Redis-Client fuer Cache, Sessions und Token-Revocation. + * + * Verwendungszwecke: + * - Token-Blocklist (JWT Revocation) + * - Session-Cache + * - Rate-Limit-Counter + * - Pub/Sub Event Bus (spaeter) + */ +@Injectable() +export class RedisService implements OnModuleInit, OnModuleDestroy { + private readonly logger = new Logger(RedisService.name); + private client!: Redis; + + constructor(private readonly config: ConfigService) {} + + async onModuleInit(): Promise { + const host = this.config.get('REDIS_HOST', 'redis'); + const port = this.config.get('REDIS_PORT', 6379); + const password = this.config.get('REDIS_PASSWORD'); + + this.client = new Redis({ + host, + port, + password: password || undefined, + maxRetriesPerRequest: 3, + retryStrategy: (times: number) => { + if (times > 10) { + this.logger.error( + 'Redis: Maximale Verbindungsversuche erreicht. Gebe auf.', + ); + return null; + } + return Math.min(times * 200, 5000); + }, + lazyConnect: true, + }); + + this.client.on('error', (err: Error) => { + this.logger.error(`Redis Fehler: ${err.message}`); + }); + + this.client.on('connect', () => { + this.logger.log('Redis Verbindung hergestellt.'); + }); + + await this.client.connect(); + } + + async onModuleDestroy(): Promise { + this.logger.log('Trenne Redis Verbindung...'); + await this.client.quit(); + } + + /** + * Ping - Verbindungstest + */ + async ping(): Promise { + return this.client.ping(); + } + + /** + * Token in die Blocklist aufnehmen (JWT Revocation). + * TTL = Restlaufzeit des Tokens. + */ + async blockToken(jti: string, ttlSeconds: number): Promise { + await this.client.set(`blocked:${jti}`, '1', 'EX', ttlSeconds); + } + + /** + * Pruefen ob ein Token blockiert ist. + */ + async isTokenBlocked(jti: string): Promise { + const result = await this.client.get(`blocked:${jti}`); + return result !== null; + } + + /** + * Refresh-Token-Familie speichern (fuer Token-Rotation-Detection). + */ + async setRefreshTokenFamily( + userId: string, + familyId: string, + ttlSeconds: number, + ): Promise { + await this.client.set( + `refresh_family:${userId}:${familyId}`, + '1', + 'EX', + ttlSeconds, + ); + } + + /** + * Pruefen ob Refresh-Token-Familie gueltig ist. + */ + async isRefreshTokenFamilyValid( + userId: string, + familyId: string, + ): Promise { + const result = await this.client.get( + `refresh_family:${userId}:${familyId}`, + ); + return result !== null; + } + + /** + * Alle Refresh-Token-Familien eines Users invalidieren (Logout-All). + */ + async invalidateAllRefreshTokens(userId: string): Promise { + const keys = await this.client.keys(`refresh_family:${userId}:*`); + if (keys.length > 0) { + await this.client.del(...keys); + } + } + + /** + * Generischer Get/Set/Del fuer Cache-Operationen. + */ + async get(key: string): Promise { + return this.client.get(key); + } + + async set(key: string, value: string, ttlSeconds?: number): Promise { + if (ttlSeconds) { + await this.client.set(key, value, 'EX', ttlSeconds); + } else { + await this.client.set(key, value); + } + } + + async del(key: string): Promise { + await this.client.del(key); + } +} diff --git a/packages/core-service/tsconfig.build.json b/packages/core-service/tsconfig.build.json new file mode 100644 index 0000000..2fe1df2 --- /dev/null +++ b/packages/core-service/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/packages/core-service/tsconfig.json b/packages/core-service/tsconfig.json new file mode 100644 index 0000000..5479b57 --- /dev/null +++ b/packages/core-service/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2022", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test"] +} diff --git a/packages/frontend/.dockerignore b/packages/frontend/.dockerignore new file mode 100644 index 0000000..2dd83c2 --- /dev/null +++ b/packages/frontend/.dockerignore @@ -0,0 +1,6 @@ +node_modules +dist +.env +*.md +.git +.gitignore diff --git a/packages/frontend/Dockerfile b/packages/frontend/Dockerfile new file mode 100644 index 0000000..a9615db --- /dev/null +++ b/packages/frontend/Dockerfile @@ -0,0 +1,34 @@ +# ============================================================ +# INSIGHT Frontend - Multi-Stage Dockerfile +# ============================================================ + +# --- Base Stage --- +FROM node:20-alpine AS base +WORKDIR /app + +# --- Dependencies Stage --- +FROM base AS deps +COPY package.json package-lock.json* ./ +RUN npm ci + +# --- Development Stage --- +FROM base AS development +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +EXPOSE 8080 +CMD ["npm", "run", "dev"] + +# --- Build Stage --- +FROM base AS build +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN npm run build + +# --- Production Stage --- +FROM nginx:alpine AS production +COPY --from=build /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 8080 +CMD ["nginx", "-g", "daemon off;"] diff --git a/packages/frontend/index.html b/packages/frontend/index.html new file mode 100644 index 0000000..a066c70 --- /dev/null +++ b/packages/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + INSIGHT Platform + + +
+ + + diff --git a/packages/frontend/nginx.conf b/packages/frontend/nginx.conf new file mode 100644 index 0000000..1e61cda --- /dev/null +++ b/packages/frontend/nginx.conf @@ -0,0 +1,33 @@ +server { + listen 8080; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # SPA: Alle Routen auf index.html weiterleiten + location / { + try_files $uri $uri/ /index.html; + } + + # Security Headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # Caching fuer Assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Kein Caching fuer index.html + location = /index.html { + add_header Cache-Control "no-cache, no-store, must-revalidate"; + } + + # Gzip + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml; + gzip_min_length 1000; +} diff --git a/packages/frontend/package.json b/packages/frontend/package.json new file mode 100644 index 0000000..9cd95dd --- /dev/null +++ b/packages/frontend/package.json @@ -0,0 +1,34 @@ +{ + "name": "@insight/frontend", + "version": "0.1.0", + "description": "INSIGHT MVP - Frontend (React + Vite)", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "lint": "eslint . --ext ts,tsx --fix", + "lint:check": "eslint . --ext ts,tsx", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "react": "^18.3.0", + "react-dom": "^18.3.0", + "react-router-dom": "^6.26.0", + "axios": "^1.7.0", + "@tanstack/react-query": "^5.56.0" + }, + "devDependencies": { + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", + "@vitejs/plugin-react": "^4.3.0", + "eslint": "^9.0.0", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.0", + "typescript": "^5.6.0", + "vite": "^6.0.0" + } +} diff --git a/packages/frontend/src/admin/.gitkeep b/packages/frontend/src/admin/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/packages/frontend/src/admin/AdminTenantsPage.tsx b/packages/frontend/src/admin/AdminTenantsPage.tsx new file mode 100644 index 0000000..30e13c5 --- /dev/null +++ b/packages/frontend/src/admin/AdminTenantsPage.tsx @@ -0,0 +1,96 @@ +import { useQuery } from '@tanstack/react-query'; +import api from '../api/client'; + +interface Tenant { + id: string; + name: string; + slug: string; + isActive: boolean; + memberCount: number; + createdAt: string; +} + +interface TenantsResponse { + data: Tenant[]; + meta: { + total: number; + page: number; + limit: number; + totalPages: number; + }; +} + +export function AdminTenantsPage() { + const { data, isLoading, error } = useQuery({ + queryKey: ['admin', 'tenants'], + queryFn: async () => { + const response = await api.get('/tenants'); + return response.data; + }, + }); + + if (isLoading) return

Laden...

; + if (error) return

Fehler beim Laden der Mandanten

; + + return ( +
+
+

Mandantenverwaltung

+ + {data?.meta.total ?? 0} Mandanten gesamt + +
+ +
+ + + + + + + + + + + + {data?.data.map((tenant) => ( + + + + + + + + ))} + +
NameSlugMitgliederStatusErstellt
+ {tenant.name} + + + {tenant.slug} + + + {tenant.memberCount} + + + {tenant.isActive ? 'Aktiv' : 'Inaktiv'} + + {new Date(tenant.createdAt).toLocaleDateString('de-DE')} +
+
+
+ ); +} diff --git a/packages/frontend/src/admin/AdminUsersPage.tsx b/packages/frontend/src/admin/AdminUsersPage.tsx new file mode 100644 index 0000000..2f510b3 --- /dev/null +++ b/packages/frontend/src/admin/AdminUsersPage.tsx @@ -0,0 +1,106 @@ +import { useQuery } from '@tanstack/react-query'; +import api from '../api/client'; + +interface User { + id: string; + email: string; + firstName: string; + lastName: string; + role: string; + isActive: boolean; + lastLogin: string | null; + createdAt: string; +} + +interface UsersResponse { + data: User[]; + meta: { + total: number; + page: number; + limit: number; + totalPages: number; + }; +} + +export function AdminUsersPage() { + const { data, isLoading, error } = useQuery({ + queryKey: ['admin', 'users'], + queryFn: async () => { + const response = await api.get('/users'); + return response.data; + }, + }); + + if (isLoading) return

Laden...

; + if (error) return

Fehler beim Laden der Benutzer

; + + return ( +
+
+

Benutzerverwaltung

+ + {data?.meta.total ?? 0} Benutzer gesamt + +
+ +
+ + + + + + + + + + + + {data?.data.map((user) => ( + + + + + + + + ))} + +
NameE-MailRolleStatusLetzter Login
+ {user.firstName} {user.lastName} + + {user.email} + + + {user.role} + + + + {user.isActive ? 'Aktiv' : 'Inaktiv'} + + {user.lastLogin ? new Date(user.lastLogin).toLocaleDateString('de-DE') : 'Nie'} +
+
+
+ ); +} diff --git a/packages/frontend/src/api/client.ts b/packages/frontend/src/api/client.ts new file mode 100644 index 0000000..77f704e --- /dev/null +++ b/packages/frontend/src/api/client.ts @@ -0,0 +1,76 @@ +import axios from 'axios'; + +/** + * Axios-Client mit automatischem Token-Handling. + * + * SICHERHEITSREGEL: Access-Token wird NUR im Memory gehalten (Variable). + * Kein localStorage! Refresh-Token kommt als HttpOnly Cookie. + */ + +// Access-Token im Memory (nicht localStorage!) +let accessToken: string | null = null; + +export const setAccessToken = (token: string | null): void => { + accessToken = token; +}; + +export const getAccessToken = (): string | null => { + return accessToken; +}; + +// API-Client +const api = axios.create({ + baseURL: '/api/v1', + withCredentials: true, // HttpOnly Cookie mitsenden + headers: { + 'Content-Type': 'application/json', + }, +}); + +// Request-Interceptor: Access-Token anfuegen +api.interceptors.request.use((config) => { + if (accessToken) { + config.headers.Authorization = `Bearer ${accessToken}`; + } + return config; +}); + +// Response-Interceptor: Bei 401 automatisch Token erneuern (Silent Refresh) +api.interceptors.response.use( + (response) => response, + async (error) => { + const originalRequest = error.config; + + // Bei 401 und noch kein Retry versucht + if ( + error.response?.status === 401 && + !originalRequest._retry && + originalRequest.url !== '/auth/refresh' && + originalRequest.url !== '/auth/login' + ) { + originalRequest._retry = true; + + try { + // Silent Refresh via HttpOnly Cookie + const { data } = await axios.post<{ accessToken: string }>( + '/api/v1/auth/refresh', + {}, + { withCredentials: true }, + ); + + setAccessToken(data.accessToken); + originalRequest.headers.Authorization = `Bearer ${data.accessToken}`; + return api(originalRequest); + } catch { + // Refresh fehlgeschlagen: Logout + setAccessToken(null); + window.location.href = '/login'; + return Promise.reject(error); + } + } + + return Promise.reject(error); + }, +); + +export default api; diff --git a/packages/frontend/src/auth/.gitkeep b/packages/frontend/src/auth/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/packages/frontend/src/auth/AuthContext.tsx b/packages/frontend/src/auth/AuthContext.tsx new file mode 100644 index 0000000..de423c7 --- /dev/null +++ b/packages/frontend/src/auth/AuthContext.tsx @@ -0,0 +1,130 @@ +import { + createContext, + useContext, + useState, + useCallback, + useEffect, + type ReactNode, +} from 'react'; +import api, { setAccessToken } from '../api/client'; + +interface User { + id: string; + email: string; + firstName: string; + lastName: string; + role: string; +} + +interface AuthContextType { + user: User | null; + isAuthenticated: boolean; + isLoading: boolean; + login: (email: string, password: string, totpCode?: string) => Promise; + logout: () => Promise; +} + +interface LoginResult { + success: boolean; + requiresTwoFactor?: boolean; + error?: string; +} + +const AuthContext = createContext(null); + +export function AuthProvider({ children }: { children: ReactNode }) { + const [user, setUser] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + // Beim Start: Silent Refresh versuchen + useEffect(() => { + const initAuth = async () => { + try { + const { data } = await api.post<{ accessToken: string }>( + '/auth/refresh', + ); + setAccessToken(data.accessToken); + + // User-Profil laden + const profileResponse = await api.get('/users/me'); + setUser(profileResponse.data); + } catch { + // Nicht eingeloggt - normal + setAccessToken(null); + setUser(null); + } finally { + setIsLoading(false); + } + }; + + initAuth(); + }, []); + + const login = useCallback( + async ( + email: string, + password: string, + totpCode?: string, + ): Promise => { + try { + const { data } = await api.post<{ + accessToken?: string; + user?: User; + requiresTwoFactor?: boolean; + }>('/auth/login', { email, password, totpCode }); + + if (data.requiresTwoFactor) { + return { success: false, requiresTwoFactor: true }; + } + + if (data.accessToken && data.user) { + setAccessToken(data.accessToken); + setUser(data.user); + return { success: true }; + } + + return { success: false, error: 'Unerwartete Antwort vom Server' }; + } catch (err: unknown) { + const error = err as { response?: { data?: { message?: string } } }; + return { + success: false, + error: error.response?.data?.message ?? 'Login fehlgeschlagen', + }; + } + }, + [], + ); + + const logout = useCallback(async () => { + try { + await api.post('/auth/logout'); + } catch { + // Fehler ignorieren + } finally { + setAccessToken(null); + setUser(null); + } + }, []); + + return ( + + {children} + + ); +} + +export function useAuth(): AuthContextType { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth muss innerhalb von AuthProvider verwendet werden'); + } + return context; +} diff --git a/packages/frontend/src/auth/LoginPage.module.css b/packages/frontend/src/auth/LoginPage.module.css new file mode 100644 index 0000000..37206ab --- /dev/null +++ b/packages/frontend/src/auth/LoginPage.module.css @@ -0,0 +1,106 @@ +.container { + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + background: linear-gradient(135deg, #1a56db 0%, #1e3a5f 100%); +} + +.card { + background: var(--color-bg-card); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-md); + padding: 2.5rem; + width: 100%; + max-width: 400px; +} + +.logo { + text-align: center; + margin-bottom: 2rem; +} + +.logo h1 { + font-size: 2rem; + font-weight: 700; + color: var(--color-primary); + letter-spacing: 2px; +} + +.logo p { + color: var(--color-text-secondary); + font-size: 0.875rem; + margin-top: 0.25rem; +} + +.form { + display: flex; + flex-direction: column; + gap: 1.25rem; +} + +.field { + display: flex; + flex-direction: column; + gap: 0.375rem; +} + +.field label { + font-size: 0.875rem; + font-weight: 500; + color: var(--color-text); +} + +.field input { + padding: 0.625rem 0.75rem; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + font-size: 0.9375rem; + transition: border-color 0.15s; +} + +.field input:focus { + outline: none; + border-color: var(--color-primary); + box-shadow: 0 0 0 3px var(--color-primary-light); +} + +.field input:disabled { + background: var(--color-bg); + color: var(--color-text-muted); +} + +.field small { + color: var(--color-text-muted); + font-size: 0.75rem; +} + +.button { + padding: 0.75rem; + background: var(--color-primary); + color: white; + border: none; + border-radius: var(--radius-sm); + font-size: 1rem; + font-weight: 600; + transition: background 0.15s; + margin-top: 0.5rem; +} + +.button:hover:not(:disabled) { + background: var(--color-primary-hover); +} + +.button:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +.error { + background: #fef2f2; + color: var(--color-error); + padding: 0.75rem; + border-radius: var(--radius-sm); + font-size: 0.875rem; + border: 1px solid #fecaca; +} diff --git a/packages/frontend/src/auth/LoginPage.tsx b/packages/frontend/src/auth/LoginPage.tsx new file mode 100644 index 0000000..1574ddb --- /dev/null +++ b/packages/frontend/src/auth/LoginPage.tsx @@ -0,0 +1,113 @@ +import { useState, type FormEvent } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from './AuthContext'; +import styles from './LoginPage.module.css'; + +export function LoginPage() { + const navigate = useNavigate(); + const { login } = useAuth(); + + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [totpCode, setTotpCode] = useState(''); + const [showTotp, setShowTotp] = useState(false); + const [error, setError] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + setError(''); + setIsSubmitting(true); + + try { + const result = await login( + email, + password, + showTotp ? totpCode : undefined, + ); + + if (result.requiresTwoFactor) { + setShowTotp(true); + setIsSubmitting(false); + return; + } + + if (result.success) { + navigate('/'); + } else { + setError(result.error ?? 'Login fehlgeschlagen'); + } + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+
+
+

INSIGHT

+

Business Platform

+
+ +
+ {error &&
{error}
} + +
+ + setEmail(e.target.value)} + placeholder="ihre@email.de" + required + autoFocus + disabled={showTotp} + /> +
+ +
+ + setPassword(e.target.value)} + placeholder="Passwort eingeben" + required + minLength={8} + disabled={showTotp} + /> +
+ + {showTotp && ( +
+ + setTotpCode(e.target.value)} + placeholder="6-stelliger Code" + maxLength={6} + pattern="[0-9]{6}" + required + autoFocus + /> + Code aus Ihrer Authenticator-App eingeben +
+ )} + + +
+
+
+ ); +} diff --git a/packages/frontend/src/components/HelpPanel/.gitkeep b/packages/frontend/src/components/HelpPanel/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/packages/frontend/src/components/HelpTooltip/.gitkeep b/packages/frontend/src/components/HelpTooltip/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/packages/frontend/src/index.css b/packages/frontend/src/index.css new file mode 100644 index 0000000..0f227f8 --- /dev/null +++ b/packages/frontend/src/index.css @@ -0,0 +1,75 @@ +/* ============================================================ + INSIGHT MVP - Globale Styles + ============================================================ */ + +:root { + /* Farben - Corporate Design */ + --color-primary: #1a56db; + --color-primary-hover: #1e40af; + --color-primary-light: #dbeafe; + --color-secondary: #6b7280; + --color-success: #059669; + --color-warning: #d97706; + --color-error: #dc2626; + + /* Graustufen */ + --color-bg: #f9fafb; + --color-bg-card: #ffffff; + --color-border: #e5e7eb; + --color-text: #111827; + --color-text-secondary: #6b7280; + --color-text-muted: #9ca3af; + + /* Layout */ + --sidebar-width: 240px; + --header-height: 56px; + + /* Schatten */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + + /* Radien */ + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, + 'Helvetica Neue', Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + color: var(--color-text); + background-color: var(--color-bg); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + min-height: 100vh; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + color: var(--color-primary); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +button { + cursor: pointer; + font-family: inherit; +} + +input, +textarea, +select { + font-family: inherit; + font-size: inherit; +} diff --git a/packages/frontend/src/main.tsx b/packages/frontend/src/main.tsx new file mode 100644 index 0000000..7ebb229 --- /dev/null +++ b/packages/frontend/src/main.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { AuthProvider } from './auth/AuthContext'; +import { App } from './shell/App'; +import './index.css'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 1, + staleTime: 5 * 60 * 1000, // 5 Minuten + refetchOnWindowFocus: false, + }, + }, +}); + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + + + + + , +); diff --git a/packages/frontend/src/shell/.gitkeep b/packages/frontend/src/shell/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/packages/frontend/src/shell/App.tsx b/packages/frontend/src/shell/App.tsx new file mode 100644 index 0000000..86b7ac9 --- /dev/null +++ b/packages/frontend/src/shell/App.tsx @@ -0,0 +1,51 @@ +import { Routes, Route, Navigate } from 'react-router-dom'; +import { useAuth } from '../auth/AuthContext'; +import { LoginPage } from '../auth/LoginPage'; +import { AppLayout } from './AppLayout'; +import { DashboardPage } from './DashboardPage'; +import { AdminUsersPage } from '../admin/AdminUsersPage'; +import { AdminTenantsPage } from '../admin/AdminTenantsPage'; + +function PrivateRoute({ children }: { children: React.ReactNode }) { + const { isAuthenticated, isLoading } = useAuth(); + + if (isLoading) { + return ( +
+

Laden...

+
+ ); + } + + if (!isAuthenticated) { + return ; + } + + return <>{children}; +} + +export function App() { + return ( + + {/* Oeffentliche Routen */} + } /> + + {/* Geschuetzte Routen */} + + + + } + > + } /> + } /> + } /> + + + {/* Fallback */} + } /> + + ); +} diff --git a/packages/frontend/src/shell/AppLayout.module.css b/packages/frontend/src/shell/AppLayout.module.css new file mode 100644 index 0000000..97bc883 --- /dev/null +++ b/packages/frontend/src/shell/AppLayout.module.css @@ -0,0 +1,105 @@ +.layout { + display: flex; + min-height: 100vh; +} + +.sidebar { + width: var(--sidebar-width); + background: #1e293b; + color: white; + display: flex; + flex-direction: column; + position: fixed; + top: 0; + left: 0; + bottom: 0; + z-index: 100; +} + +.brand { + padding: 1.25rem 1.5rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.brand h2 { + font-size: 1.25rem; + font-weight: 700; + letter-spacing: 2px; + color: #60a5fa; +} + +.nav { + flex: 1; + padding: 1rem 0; + overflow-y: auto; +} + +.navSection { + padding: 1rem 1.5rem 0.5rem; + font-size: 0.6875rem; + text-transform: uppercase; + letter-spacing: 1px; + color: rgba(255, 255, 255, 0.4); + font-weight: 600; +} + +.navLink { + display: block; + padding: 0.625rem 1.5rem; + color: rgba(255, 255, 255, 0.7); + text-decoration: none; + font-size: 0.875rem; + transition: all 0.15s; + border-left: 3px solid transparent; +} + +.navLink:hover { + color: white; + background: rgba(255, 255, 255, 0.05); + text-decoration: none; +} + +.active { + color: white; + background: rgba(96, 165, 250, 0.15); + border-left-color: #60a5fa; +} + +.userInfo { + padding: 1rem 1.5rem; + border-top: 1px solid rgba(255, 255, 255, 0.1); +} + +.userName { + font-size: 0.875rem; + font-weight: 600; +} + +.userEmail { + font-size: 0.75rem; + color: rgba(255, 255, 255, 0.5); + margin-top: 0.125rem; +} + +.logoutBtn { + margin-top: 0.75rem; + width: 100%; + padding: 0.5rem; + background: rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.8); + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: var(--radius-sm); + font-size: 0.8125rem; + transition: all 0.15s; +} + +.logoutBtn:hover { + background: rgba(255, 255, 255, 0.15); + color: white; +} + +.main { + flex: 1; + margin-left: var(--sidebar-width); + padding: 2rem; +} diff --git a/packages/frontend/src/shell/AppLayout.tsx b/packages/frontend/src/shell/AppLayout.tsx new file mode 100644 index 0000000..4b5ce29 --- /dev/null +++ b/packages/frontend/src/shell/AppLayout.tsx @@ -0,0 +1,74 @@ +import { Outlet, NavLink, useNavigate } from 'react-router-dom'; +import { useAuth } from '../auth/AuthContext'; +import styles from './AppLayout.module.css'; + +export function AppLayout() { + const { user, logout } = useAuth(); + const navigate = useNavigate(); + + const handleLogout = async () => { + await logout(); + navigate('/login'); + }; + + return ( +
+ {/* Sidebar */} + + + {/* Main Content */} +
+ +
+
+ ); +} diff --git a/packages/frontend/src/shell/DashboardPage.tsx b/packages/frontend/src/shell/DashboardPage.tsx new file mode 100644 index 0000000..b6df335 --- /dev/null +++ b/packages/frontend/src/shell/DashboardPage.tsx @@ -0,0 +1,31 @@ +import { useAuth } from '../auth/AuthContext'; + +export function DashboardPage() { + const { user } = useAuth(); + + return ( +
+

+ Dashboard +

+ +
+

+ Willkommen, {user?.firstName}! +

+

+ INSIGHT Platform - Sprint 1 Alpha +

+

+ Rolle: {user?.role} +

+
+
+ ); +} diff --git a/packages/frontend/tsconfig.json b/packages/frontend/tsconfig.json new file mode 100644 index 0000000..518e042 --- /dev/null +++ b/packages/frontend/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src", "vite-env.d.ts"] +} diff --git a/packages/frontend/vite-env.d.ts b/packages/frontend/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/packages/frontend/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts new file mode 100644 index 0000000..541e01f --- /dev/null +++ b/packages/frontend/vite.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import path from 'path'; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + server: { + port: 8080, + host: true, + proxy: { + '/api': { + target: 'http://localhost:3000', + changeOrigin: true, + }, + }, + }, + build: { + outDir: 'dist', + sourcemap: true, + }, +}); From 5412ae137a7fd60e7f2153f77cac56837eaac48a Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Sun, 8 Mar 2026 16:21:45 +0100 Subject: [PATCH 06/50] feat: adapt codebase for IP-based HTTP deployment on 172.20.10.59 Switch from hostname+HTTPS (insight-dev.xinion.lan) to IP+HTTP (172.20.10.59) for alpha/dev deployment without DNS. Key changes: - Cookie secure/sameSite flags environment-conditional (fixes HTTP auth) - docker-compose.yml: remove HTTPS, update host rules, reduce PG memory - Traefik: disable TLS, update CORS/CSP for HTTP - Add Prisma init migration (7 tables) and admin seed script - Generate package-lock.json for npm ci in Docker builds - Update all documentation for IP-based access Co-Authored-By: Claude Opus 4.6 --- .env.example | 6 +- README.md | 25 +- Summarize.md | 76 +- config/traefik/dynamic/middlewares.yml | 7 +- config/traefik/dynamic/tls.yml | 26 +- docker-compose.yml | 41 +- docs/ACCESS.md | 30 +- docs/INFRASTRUCTURE.md | 38 +- packages/core-service/package-lock.json | 10925 ++++++++++++++++ packages/core-service/package.json | 3 +- .../20260308000000_init/migration.sql | 146 + .../prisma/migrations/migration_lock.toml | 3 + packages/core-service/prisma/seed.ts | 65 + .../core-service/src/config/env.validation.ts | 2 +- .../src/core/auth/auth.controller.ts | 16 +- packages/core-service/src/main.ts | 2 +- packages/frontend/package-lock.json | 3664 ++++++ 17 files changed, 14970 insertions(+), 105 deletions(-) create mode 100644 packages/core-service/package-lock.json create mode 100644 packages/core-service/prisma/migrations/20260308000000_init/migration.sql create mode 100644 packages/core-service/prisma/migrations/migration_lock.toml create mode 100644 packages/core-service/prisma/seed.ts create mode 100644 packages/frontend/package-lock.json diff --git a/.env.example b/.env.example index 4b5c5be..41c54f6 100644 --- a/.env.example +++ b/.env.example @@ -8,8 +8,8 @@ # --- Allgemein --- NODE_ENV=development APP_PORT=3000 -APP_URL=https://insight-dev.xinion.lan -FRONTEND_URL=https://insight-dev.xinion.lan +APP_URL=http://172.20.10.59 +FRONTEND_URL=http://172.20.10.59 LOG_LEVEL=info # --- PostgreSQL --- @@ -41,7 +41,7 @@ JWT_ISSUER=insight-platform BCRYPT_COST=12 # --- CORS --- -CORS_ORIGINS=https://insight-dev.xinion.lan +CORS_ORIGINS=http://172.20.10.59 # --- Rate Limiting --- THROTTLE_TTL=60000 diff --git a/README.md b/README.md index a48c4ac..3f41f2e 100644 --- a/README.md +++ b/README.md @@ -63,9 +63,9 @@ cp .env.example .env ### 3. JWT-Schluessel generieren ```bash # RS256 Schluessel fuer JWT-Signierung -mkdir -p packages/core-service/keys -openssl genpkey -algorithm RSA -out packages/core-service/keys/jwt-private.pem -pkeyopt rsa_keygen_bits:2048 -openssl rsa -pubout -in packages/core-service/keys/jwt-private.pem -out packages/core-service/keys/jwt-public.pem +mkdir -p keys +openssl genpkey -algorithm RSA -out keys/jwt-private.pem -pkeyopt rsa_keygen_bits:2048 +openssl rsa -pubout -in keys/jwt-private.pem -out keys/jwt-public.pem ``` ### 4. Services starten @@ -77,23 +77,24 @@ docker compose up -d docker compose -f docker-compose.yml -f docker-compose.observability.yml up -d ``` -### 5. Datenbank-Migration +### 5. Datenbank-Migration + Seed ```bash -# Core-Schema -docker compose exec core npx prisma migrate deploy --schema=./prisma/core.schema.prisma +# Core-Schema migrieren +docker compose run --rm core npx prisma migrate deploy --schema=./prisma/core.schema.prisma -# Tenant-Schema (wird beim Onboarding automatisch ausgefuehrt) +# Admin-User anlegen +docker compose run --rm core npx ts-node prisma/seed.ts ``` ### 6. Health-Checks pruefen ```bash -curl http://localhost:3000/health # Core-Service -curl http://localhost:8080 # Frontend +curl http://172.20.10.59/health ``` ### 7. Erster Login -- URL: https://insight-dev.xinion.lan (oder http://localhost) -- Initialer Admin-Account wird beim ersten Start via Seed-Script angelegt +- URL: `http://172.20.10.59` +- Admin: `admin@xinion.de` / `ChangeMe123!` +- Passwort nach erstem Login aendern! --- @@ -101,7 +102,7 @@ curl http://localhost:8080 # Frontend | Service | Port (intern) | URL (extern via Traefik) | Beschreibung | |---------------|---------------|----------------------------------|------------------------| -| Traefik | 80/443 | https://insight-dev.xinion.lan | API Gateway | +| Traefik | 80 | http://172.20.10.59 | API Gateway | | Core-Service | 3000 | /api/v1/* | NestJS Backend | | Frontend | 8080 | /* | React App | | PostgreSQL | 5432 | - | Datenbank | diff --git a/Summarize.md b/Summarize.md index 4ebafca..aaa908a 100644 --- a/Summarize.md +++ b/Summarize.md @@ -67,11 +67,11 @@ **Erstellte Dateien:** 1. **`docker-compose.yml`** - Alle Basis-Services: - - Traefik 3 (API Gateway, Reverse Proxy, TLS, Rate Limiting) - - PostgreSQL 16-alpine (mit Performance-Tuning fuer 8GB RAM) + - Traefik 3 (API Gateway, Reverse Proxy, Rate Limiting) + - PostgreSQL 16-alpine (Performance-Tuning: 1GB shared_buffers, 4GB cache) - PgBouncer (Connection Pooling, Transaction Mode) - Redis 7-alpine (Cache, Sessions, Token-Revocation) - - step-ca (Interne Certificate Authority fuer mTLS) + - step-ca (Interne Certificate Authority fuer mTLS - geplant) - Core-Service (NestJS) mit Traefik-Labels - Frontend (React) mit Traefik-Labels - 3 isolierte Docker-Netzwerke (insight-web, insight-db, insight-cache) @@ -87,7 +87,7 @@ - PostgreSQL Exporter (DB-Metriken) 3. **Konfigurationsdateien:** - - `config/traefik/dynamic/tls.yml` - TLS-Konfiguration + - `config/traefik/dynamic/tls.yml` - TLS deaktiviert (Alpha/Dev) - `config/traefik/dynamic/middlewares.yml` - Security-Headers, CORS, Compression - `config/prometheus/prometheus.yml` - Scrape-Konfiguration - `config/loki/loki.yml` - Log-Storage-Konfiguration @@ -113,7 +113,7 @@ - `TotpService`: TOTP 2FA (Google Authenticator kompatibel) - `LoginDto`: Validierung mit class-validator - Account-Lockout nach 5 Fehlversuchen (15 Min Sperre) - - Refresh-Token als HttpOnly/Secure/SameSite=Strict Cookie + - Refresh-Token als HttpOnly Cookie (secure/sameSite umgebungsabhaengig) - Token-Rotation mit Redis-basierter Familien-Erkennung 2. **Users-Modul** (`src/core/users/`) @@ -145,7 +145,7 @@ 6. **Config:** - `validateConfig()` mit class-validator fuer Umgebungsvariablen -#### 6. Prisma-Schemas +#### 6. Prisma-Schemas & Migration 1. **`core.schema.prisma`** (platform_core Datenbank): - `User` - Plattform-Benutzer (mit Login-Tracking, 2FA) @@ -161,6 +161,14 @@ - `Activity` - CRM-Aktivitaeten (Notiz, Anruf, E-Mail, Meeting, Task) - Referenz-Schema fuer Sprint 2 (CRM-Modul) +3. **Erste Migration erstellt** (`prisma/migrations/20260308000000_init/`) + - 7 Tabellen, alle Indizes und Foreign Keys + - `migration_lock.toml` fuer Prisma + +4. **Seed-Script erstellt** (`prisma/seed.ts`) + - Erstellt Platform-Admin: `admin@xinion.de` / `ChangeMe123!` + - Bcrypt Cost 12, Rolle: PLATFORM_ADMIN + #### 7. React Frontend-Shell **Projekt-Setup:** @@ -211,6 +219,46 @@ - SSH-Deploy auf insight-dev-01 - Health-Check Verifizierung +#### 9. IP-basierte Deployment-Anpassung (HTTP statt HTTPS) + +**Grund:** Kein DNS-Eintrag vorhanden, Zugriff nur ueber IP 172.20.10.59. + +**Geaenderte Dateien:** + +1. **`auth.controller.ts`** - Cookie secure/sameSite umgebungsabhaengig + - `secure: true` -> `secure: process.env.NODE_ENV === 'production'` + - `sameSite: 'strict'` -> `isProduction ? 'strict' : 'lax'` + - Betrifft `setRefreshTokenCookie()` und `logout()` + +2. **`docker-compose.yml`** - HTTP + IP Umstellung + - HTTPS-Redirect entfernt + - TLS-Entrypoint deaktiviert, Port 443 entfernt + - Alle Host-Rules: `insight-dev.xinion.lan` -> `172.20.10.59` + - Alle Entrypoints: `websecure` -> `web` + - URL-Defaults auf `http://172.20.10.59` + - PostgreSQL Memory reduziert (1GB/4GB/256MB fuer 8GB RAM VM) + - JWT-Keys Volume-Mount hinzugefuegt: `./keys:/app/keys:ro` + +3. **`config/traefik/dynamic/tls.yml`** - TLS-Konfiguration deaktiviert + +4. **`config/traefik/dynamic/middlewares.yml`** + - HSTS-Headers entfernt + - CSP: `wss://insight-dev.xinion.lan` -> `ws://172.20.10.59` + - CORS: `https://insight-dev.xinion.lan` -> `http://172.20.10.59` + +5. **`main.ts`** - CORS-Fallback auf `http://172.20.10.59` + +6. **`env.validation.ts`** - APP_URL Default auf `http://172.20.10.59` + +7. **`.env.example`** - Alle URLs auf `http://172.20.10.59` + +8. **`package-lock.json`** - Generiert fuer core-service und frontend (npm ci braucht diese) + +9. **Dokumentation aktualisiert:** + - `docs/INFRASTRUCTURE.md` - HTTP statt HTTPS, IP statt DNS + - `docs/ACCESS.md` - Ports, URLs, Default-Zugangsdaten + - `README.md` - Setup-Anleitung, URLs, Seed-Befehle + --- ### Naechste Schritte @@ -222,15 +270,21 @@ - [x] Prisma-Schemas erstellen (core + tenant) - [x] React Frontend-Shell implementieren - [x] CI/CD Pipelines (.forgejo/workflows/) definieren +- [x] Codebase auf HTTP + IP (172.20.10.59) umstellen +- [x] Seed-Script erstellen (admin@xinion.de) +- [x] Prisma-Migration erstellen (init) +- [x] package-lock.json generieren +- [x] Dokumentation aktualisieren +- [ ] Commit & Push auf develop +- [ ] LVM-Festplatte auf Server erweitern (60GB -> voll nutzbar) - [ ] Docker + Docker Compose auf insight-dev-01 installieren -- [ ] .env-Datei auf Server anlegen (echte Passwoerter) -- [ ] JWT RS256 Schluessel generieren (fuer Token-Signierung) -- [ ] Erste Prisma-Migration ausfuehren -- [ ] Platform-Admin User anlegen (Seed) +- [ ] Repo klonen, .env + JWT-Keys auf Server erstellen +- [ ] Services starten, Migration + Seed ausfuehren - [ ] Erster End-to-End Test (Login -> Dashboard) --- ### Offene Fragen / Abhaengigkeiten -- DNS-Eintrag `insight-dev.xinion.lan` muss auf 172.20.10.59 zeigen +- DNS-Eintrag `insight-dev.xinion.lan` wird spaeter eingerichtet (dann HTTPS aktivieren) +- LVM auf Server muss erweitert werden (60GB Disk, nur ~56GB sichtbar) diff --git a/config/traefik/dynamic/middlewares.yml b/config/traefik/dynamic/middlewares.yml index 9e2c67e..4c06668 100644 --- a/config/traefik/dynamic/middlewares.yml +++ b/config/traefik/dynamic/middlewares.yml @@ -10,9 +10,6 @@ http: browserXssFilter: true contentTypeNosniff: true frameDeny: true - stsIncludeSubdomains: true - stsPreload: true - stsSeconds: 31536000 customFrameOptionsValue: "SAMEORIGIN" referrerPolicy: "strict-origin-when-cross-origin" contentSecurityPolicy: >- @@ -21,7 +18,7 @@ http: style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; - connect-src 'self' wss://insight-dev.xinion.lan; + connect-src 'self' ws://172.20.10.59; frame-ancestors 'self'; # CORS fuer API @@ -40,7 +37,7 @@ http: - X-Tenant-ID - X-Request-ID accessControlAllowOriginList: - - "https://insight-dev.xinion.lan" + - "http://172.20.10.59" accessControlMaxAge: 86400 accessControlAllowCredentials: true addVaryHeader: true diff --git a/config/traefik/dynamic/tls.yml b/config/traefik/dynamic/tls.yml index 8fed996..42fcfd4 100644 --- a/config/traefik/dynamic/tls.yml +++ b/config/traefik/dynamic/tls.yml @@ -1,24 +1,2 @@ -# ============================================================ -# Traefik - Dynamische TLS-Konfiguration -# ============================================================ -# Fuer die Alpha-Phase verwenden wir ein selbst-signiertes -# Zertifikat. Spaeter wird step-ca als ACME-Provider genutzt. -# ============================================================ - -tls: - options: - default: - minVersion: VersionTLS12 - cipherSuites: - - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 - - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 - - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 - - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 - sniStrict: false - - stores: - default: - defaultGeneratedCert: - resolver: default - domain: - main: "insight-dev.xinion.lan" +# TLS-Konfiguration deaktiviert fuer Alpha/Dev (IP-basierter HTTP-Zugang). +# Wird reaktiviert wenn DNS + HTTPS eingerichtet wird. diff --git a/docker-compose.yml b/docker-compose.yml index b092fc2..2fc08db 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -45,12 +45,8 @@ services: # API & Dashboard - "--api.dashboard=true" - "--api.insecure=true" - # Entrypoints + # Entrypoints (nur HTTP fuer Alpha/Dev mit IP-Zugang) - "--entrypoints.web.address=:80" - - "--entrypoints.websecure.address=:443" - # HTTP -> HTTPS Redirect - - "--entrypoints.web.http.redirections.entryPoint.to=websecure" - - "--entrypoints.web.http.redirections.entryPoint.scheme=https" # Docker Provider - "--providers.docker=true" - "--providers.docker.exposedbydefault=false" @@ -58,8 +54,6 @@ services: # File Provider (fuer dynamische Konfiguration) - "--providers.file.directory=/etc/traefik/dynamic" - "--providers.file.watch=true" - # TLS (self-signed fuer Dev) - - "--entrypoints.websecure.http.tls=true" # Logging - "--log.level=INFO" - "--accesslog=true" @@ -70,7 +64,6 @@ services: - "--entrypoints.metrics.address=:8082" ports: - "80:80" - - "443:443" - "8080:8080" # Dashboard (nur intern) volumes: - /var/run/docker.sock:/var/run/docker.sock:ro @@ -80,10 +73,10 @@ services: - insight-web labels: - "traefik.enable=true" - # Dashboard Route (nur intern) - - "traefik.http.routers.dashboard.rule=Host(`traefik.insight-dev.xinion.lan`)" + # Dashboard Route (nur intern, via Port 8080) + - "traefik.http.routers.dashboard.rule=Host(`172.20.10.59`) && PathPrefix(`/dashboard`)" - "traefik.http.routers.dashboard.service=api@internal" - - "traefik.http.routers.dashboard.entrypoints=websecure" + - "traefik.http.routers.dashboard.entrypoints=web" healthcheck: test: ["CMD", "traefik", "healthcheck"] interval: 30s @@ -118,13 +111,13 @@ services: command: - "postgres" - "-c" - - "shared_buffers=2GB" + - "shared_buffers=1GB" - "-c" - - "effective_cache_size=6GB" + - "effective_cache_size=4GB" - "-c" - "work_mem=16MB" - "-c" - - "maintenance_work_mem=512MB" + - "maintenance_work_mem=256MB" - "-c" - "max_connections=200" - "-c" @@ -225,8 +218,8 @@ services: environment: NODE_ENV: ${NODE_ENV:-development} APP_PORT: ${APP_PORT:-3000} - APP_URL: ${APP_URL:-https://insight-dev.xinion.lan} - FRONTEND_URL: ${FRONTEND_URL:-https://insight-dev.xinion.lan} + APP_URL: ${APP_URL:-http://172.20.10.59} + FRONTEND_URL: ${FRONTEND_URL:-http://172.20.10.59} LOG_LEVEL: ${LOG_LEVEL:-info} # Database (via PgBouncer) DATABASE_URL: "postgresql://${DB_USER:-insight}:${DB_PASSWORD}@pgbouncer:6432/${DB_NAME:-platform_core}" @@ -245,10 +238,12 @@ services: # Bcrypt BCRYPT_COST: ${BCRYPT_COST:-12} # CORS - CORS_ORIGINS: ${CORS_ORIGINS:-https://insight-dev.xinion.lan} + CORS_ORIGINS: ${CORS_ORIGINS:-http://172.20.10.59} # Rate Limiting THROTTLE_TTL: ${THROTTLE_TTL:-60000} THROTTLE_LIMIT: ${THROTTLE_LIMIT:-200} + volumes: + - ./keys:/app/keys:ro networks: - insight-web - insight-db @@ -261,13 +256,13 @@ services: labels: - "traefik.enable=true" # API Routing - - "traefik.http.routers.core-api.rule=Host(`insight-dev.xinion.lan`) && PathPrefix(`/api`)" - - "traefik.http.routers.core-api.entrypoints=websecure" + - "traefik.http.routers.core-api.rule=Host(`172.20.10.59`) && PathPrefix(`/api`)" + - "traefik.http.routers.core-api.entrypoints=web" - "traefik.http.routers.core-api.service=core-api" - "traefik.http.services.core-api.loadbalancer.server.port=3000" # Health-Endpunkt (ohne Auth) - - "traefik.http.routers.core-health.rule=Host(`insight-dev.xinion.lan`) && Path(`/health`)" - - "traefik.http.routers.core-health.entrypoints=websecure" + - "traefik.http.routers.core-health.rule=Host(`172.20.10.59`) && Path(`/health`)" + - "traefik.http.routers.core-health.entrypoints=web" - "traefik.http.routers.core-health.service=core-api" # Rate Limiting Middleware - "traefik.http.middlewares.api-ratelimit.ratelimit.average=100" @@ -295,8 +290,8 @@ services: labels: - "traefik.enable=true" # Frontend Routing (Catch-All nach API) - - "traefik.http.routers.frontend.rule=Host(`insight-dev.xinion.lan`)" - - "traefik.http.routers.frontend.entrypoints=websecure" + - "traefik.http.routers.frontend.rule=Host(`172.20.10.59`)" + - "traefik.http.routers.frontend.entrypoints=web" - "traefik.http.routers.frontend.service=frontend" - "traefik.http.routers.frontend.priority=1" - "traefik.http.services.frontend.loadbalancer.server.port=8080" diff --git a/docs/ACCESS.md b/docs/ACCESS.md index dfd8b48..bf5b92b 100644 --- a/docs/ACCESS.md +++ b/docs/ACCESS.md @@ -118,10 +118,11 @@ docker compose restart core ## 4. Service-Ports (auf der VM) +> **Alpha/Dev:** Kein HTTPS, kein DNS. Zugriff via `http://172.20.10.59` + | Service | Interner Port | Externer Port | URL | |-----------------|---------------|---------------|----------------------------------| -| Traefik (HTTP) | 80 | 80 | http://insight-dev.xinion.lan | -| Traefik (HTTPS) | 443 | 443 | https://insight-dev.xinion.lan | +| Traefik (HTTP) | 80 | 80 | http://172.20.10.59 | | Traefik Dashboard | 8080 | - | Nur intern | | Core-Service | 3000 | - | Via Traefik: /api/v1/* | | Frontend | 8080 | - | Via Traefik: /* | @@ -210,7 +211,20 @@ Laufende Anwendung --- -## 9. Wichtige Befehle +## 9. Default-Zugangsdaten (Alpha/Dev) + +> **WICHTIG:** Diese Zugangsdaten gelten nur fuer die Ersteinrichtung! +> Passwoerter muessen nach dem ersten Login geaendert werden. + +| Service | User / E-Mail | Passwort | +|-------------------|------------------------|--------------------| +| Plattform-Admin | `admin@xinion.de` | `ChangeMe123!` | +| Grafana | `admin` | Siehe `.env` | +| Traefik Dashboard | `admin` | Siehe `.env` | + +--- + +## 10. Wichtige Befehle ### Vom MacBook aus ```bash @@ -220,6 +234,9 @@ git push origin develop # SSH auf Server ssh -i .keys/deploy_ed25519 deploy@172.20.10.59 +# Plattform oeffnen +open http://172.20.10.59 + # Grafana oeffnen (SSH-Tunnel) ssh -L 3001:localhost:3001 -i .keys/deploy_ed25519 deploy@172.20.10.59 # Dann im Browser: http://localhost:3001 @@ -234,10 +251,13 @@ docker compose up -d docker compose -f docker-compose.yml -f docker-compose.observability.yml up -d # Health-Check -curl http://localhost:3000/health +curl http://172.20.10.59/health # Datenbank-Migration -docker compose exec core npx prisma migrate deploy +docker compose run --rm core npx prisma migrate deploy --schema=./prisma/core.schema.prisma + +# Admin-User seeden +docker compose run --rm core npx ts-node prisma/seed.ts # Logs folgen docker compose logs -f --tail=100 diff --git a/docs/INFRASTRUCTURE.md b/docs/INFRASTRUCTURE.md index 71d2c44..f583a1e 100644 --- a/docs/INFRASTRUCTURE.md +++ b/docs/INFRASTRUCTURE.md @@ -25,12 +25,14 @@ Alle Services werden als Docker-Container betrieben. - SSH: nur Key-basiert (`PasswordAuthentication no`) - Firewall (ufw): - Port 22 (SSH) - nur internes Netzwerk - - Port 80 (HTTP -> Redirect auf HTTPS) - - Port 443 (HTTPS) + - Port 80 (HTTP) - Webzugang (kein HTTPS in Alpha/Dev) - Alle anderen Ports: DENY - Automatische Sicherheitsupdates: `unattended-upgrades` aktiviert - Fail2ban fuer SSH-Brute-Force-Schutz +> **Hinweis:** In der Alpha/Dev-Phase wird kein HTTPS verwendet. +> Zugriff erfolgt ueber `http://172.20.10.59` (IP-basiert, kein DNS). + --- ## 3. Software auf der VM @@ -54,11 +56,11 @@ Alles laeuft in Containern. ``` Internet / Internes Netz | - [ Port 80/443 ] + [ Port 80 ] | +-------v--------+ - | Traefik | API Gateway, SSL-Terminierung, - | (Gateway) | Rate Limiting, mTLS-Terminierung + | Traefik | API Gateway, Reverse Proxy, + | (Gateway) | Rate Limiting +---+-------+----+ | | +---------+ +---------+ @@ -88,12 +90,12 @@ Alles laeuft in Containern. | `insight-db` | Core-Service <-> PgBouncer <-> PostgreSQL (intern) | | `insight-cache`| Core-Service <-> Redis (intern) | -### mTLS (step-ca) +### mTLS (step-ca) - geplant fuer Produktion -Alle interne Kommunikation zwischen Containern wird ueber mTLS abgesichert. -step-ca (Smallstep) fungiert als interne Certificate Authority. +> **Status:** mTLS ist in der Alpha/Dev-Phase deaktiviert. +> step-ca wird spaeter fuer interne Container-Kommunikation eingesetzt. -| Komponente | Zertifikat | +| Komponente | Zertifikat (geplant) | |---------------|-------------------------------| | Traefik | Wildcard fuer externe Domain | | Core-Service | `core-service.insight.local` | @@ -108,7 +110,7 @@ step-ca (Smallstep) fungiert als interne Certificate Authority. | Service | Image | Port (intern) | Port (extern) | Beschreibung | |---------------|--------------------------------|---------------|---------------|-------------------------------| -| `traefik` | traefik:3 | 80, 443, 8080 | 80, 443 | API Gateway, Reverse Proxy | +| `traefik` | traefik:3 | 80, 8080 | 80 | API Gateway, Reverse Proxy | | `core` | insight-core:latest | 3000 | - | NestJS Backend | | `frontend` | insight-frontend:latest | 8080 | - | React App (Nginx served) | | `postgres` | postgres:16-alpine | 5432 | - | Datenbank | @@ -149,12 +151,22 @@ PostgreSQL-Server --- -## 8. DNS / Domains +## 8. Netzwerk / Zugriff + +> **Alpha/Dev-Phase:** Kein DNS, Zugriff ueber IP-Adresse. +> HTTPS wird spaeter mit DNS-Eintrag aktiviert. + +| Zugriff | URL | Zweck | +|----------------------------|--------------------------------|-------------------------------| +| Frontend + API | `http://172.20.10.59` | Entwicklungs-Plattform | +| API-Endpunkte | `http://172.20.10.59/api/v1/*` | REST API | +| Git-Server | `git.xinion.lan` | Git Repository & CI/CD | + +### Spaeter (mit DNS): | Eintrag | Ziel | Zweck | |----------------------------|--------------------|-------------------------------| -| `insight-dev.xinion.lan` | VM-IP | Entwicklungs-Frontend | -| `api.insight-dev.xinion.lan` | VM-IP | API-Endpunkt | +| `insight-dev.xinion.lan` | VM-IP | Entwicklungs-Frontend (HTTPS) | | `git.xinion.lan` | Forgejo-Server | Git Repository & CI/CD | --- diff --git a/packages/core-service/package-lock.json b/packages/core-service/package-lock.json new file mode 100644 index 0000000..da731e0 --- /dev/null +++ b/packages/core-service/package-lock.json @@ -0,0 +1,10925 @@ +{ + "name": "@insight/core-service", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@insight/core-service", + "version": "0.1.0", + "license": "UNLICENSED", + "dependencies": { + "@nestjs/common": "^10.4.0", + "@nestjs/config": "^3.2.0", + "@nestjs/core": "^10.4.0", + "@nestjs/jwt": "^10.2.0", + "@nestjs/passport": "^10.0.3", + "@nestjs/platform-express": "^10.4.0", + "@nestjs/schedule": "^4.1.0", + "@nestjs/swagger": "^7.4.0", + "@nestjs/throttler": "^6.2.0", + "@prisma/client": "^6.4.0", + "bcrypt": "^5.1.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "cookie-parser": "^1.4.7", + "helmet": "^8.0.0", + "ioredis": "^5.4.1", + "otplib": "^12.0.1", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "qrcode": "^1.5.4", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "uuid": "^10.0.0" + }, + "devDependencies": { + "@nestjs/cli": "^10.4.0", + "@nestjs/schematics": "^10.1.0", + "@nestjs/testing": "^10.4.0", + "@types/bcrypt": "^5.0.2", + "@types/cookie-parser": "^1.4.7", + "@types/express": "^5.0.0", + "@types/jest": "^29.5.12", + "@types/node": "^22.0.0", + "@types/passport-jwt": "^4.0.1", + "@types/qrcode": "^1.5.5", + "@types/uuid": "^10.0.0", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", + "eslint": "^9.0.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.0", + "jest": "^29.7.0", + "prettier": "^3.3.0", + "prisma": "^6.4.0", + "source-map-support": "^0.5.21", + "ts-jest": "^29.2.0", + "ts-loader": "^9.5.0", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.6.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.11.tgz", + "integrity": "sha512-vTNDYNsLIWpYk2I969LMQFH29GTsLzxNk/0cLw5q56ARF0v5sIWfHYwGTS88jdDqIpuuettcSczbxeA7EuAmqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.1", + "picomatch": "4.0.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.11.tgz", + "integrity": "sha512-I5wviiIqiFwar9Pdk30Lujk8FczEEc18i22A5c6Z9lbmhPQdTroDnEQdsfXjy404wPe8H62s0I15o4pmMGfTYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "jsonc-parser": "3.2.1", + "magic-string": "0.30.8", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-17.3.11.tgz", + "integrity": "sha512-kcOMqp+PHAKkqRad7Zd7PbpqJ0LqLaNZdY1+k66lLWmkEBozgq8v4ASn/puPWf9Bo0HpCiK+EzLf0VHE8Z/y6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "@angular-devkit/schematics": "17.3.11", + "ansi-colors": "4.1.3", + "inquirer": "9.2.15", + "symbol-observable": "4.0.0", + "yargs-parser": "21.1.1" + }, + "bin": { + "schematics": "bin/schematics.js" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/inquirer": { + "version": "9.2.15", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.15.tgz", + "integrity": "sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ljharb/through": "^2.3.12", + "ansi-escapes": "^4.3.2", + "chalk": "^5.3.0", + "cli-cursor": "^3.1.0", + "cli-width": "^4.1.0", + "external-editor": "^3.1.0", + "figures": "^3.2.0", + "lodash": "^4.17.21", + "mute-stream": "1.0.0", + "ora": "^5.4.1", + "run-async": "^3.0.0", + "rxjs": "^7.8.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@borewit/text-codec": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.1.tgz", + "integrity": "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@ioredis/commands": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", + "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/reporters/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@ljharb/through": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.14.tgz", + "integrity": "sha512-ajBvlKpWucBB17FuQYUShqpqy8GRgYEpJW0vWJbUu1CV9lWyrDCapy0lScU8T8Z6qn49sSwJB3+M+evYIdGg+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@microsoft/tsdoc": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz", + "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==", + "license": "MIT" + }, + "node_modules/@nestjs/cli": { + "version": "10.4.9", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.9.tgz", + "integrity": "sha512-s8qYd97bggqeK7Op3iD49X2MpFtW4LVNLAwXFkfbRxKME6IYT7X0muNTJ2+QfI8hpbNx9isWkrLWIp+g5FOhiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "@angular-devkit/schematics": "17.3.11", + "@angular-devkit/schematics-cli": "17.3.11", + "@nestjs/schematics": "^10.0.1", + "chalk": "4.1.2", + "chokidar": "3.6.0", + "cli-table3": "0.6.5", + "commander": "4.1.1", + "fork-ts-checker-webpack-plugin": "9.0.2", + "glob": "10.4.5", + "inquirer": "8.2.6", + "node-emoji": "1.11.0", + "ora": "5.4.1", + "tree-kill": "1.2.2", + "tsconfig-paths": "4.2.0", + "tsconfig-paths-webpack-plugin": "4.2.0", + "typescript": "5.7.2", + "webpack": "5.97.1", + "webpack-node-externals": "3.0.0" + }, + "bin": { + "nest": "bin/nest.js" + }, + "engines": { + "node": ">= 16.14" + }, + "peerDependencies": { + "@swc/cli": "^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0", + "@swc/core": "^1.3.62" + }, + "peerDependenciesMeta": { + "@swc/cli": { + "optional": true + }, + "@swc/core": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nestjs/cli/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@nestjs/cli/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@nestjs/cli/node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@nestjs/cli/node_modules/webpack": { + "version": "5.97.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", + "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/@nestjs/common": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.22.tgz", + "integrity": "sha512-fxJ4v85nDHaqT1PmfNCQ37b/jcv2OojtXTaK1P2uAXhzLf9qq6WNUOFvxBrV4fhQek1EQoT1o9oj5xAZmv3NRw==", + "license": "MIT", + "peer": true, + "dependencies": { + "file-type": "20.4.1", + "iterare": "1.2.1", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/config": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.3.0.tgz", + "integrity": "sha512-pdGTp8m9d0ZCrjTpjkUbZx6gyf2IKf+7zlkrPNMsJzYZ4bFRRTpXrnj+556/5uiI6AfL5mMrJc2u7dB6bvM+VA==", + "license": "MIT", + "dependencies": { + "dotenv": "16.4.5", + "dotenv-expand": "10.0.0", + "lodash": "4.17.21" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/core": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.22.tgz", + "integrity": "sha512-6IX9+VwjiKtCjx+mXVPncpkQ5ZjKfmssOZPFexmT+6T9H9wZ3svpYACAo7+9e7Nr9DZSoRZw3pffkJP7Z0UjaA==", + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@nuxtjs/opencollective": "0.3.2", + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "3.3.0", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/websockets": "^10.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } + }, + "node_modules/@nestjs/jwt": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.2.0.tgz", + "integrity": "sha512-x8cG90SURkEiLOehNaN2aRlotxT0KZESUliOPKKnjWiyJOcWurkF3w345WOX0P4MgFzUjGoZ1Sy0aZnxeihT0g==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "9.0.5", + "jsonwebtoken": "9.0.2" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/@nestjs/mapped-types": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.5.tgz", + "integrity": "sha512-bSJv4pd6EY99NX9CjBIyn4TVDoSit82DUZlL4I3bqNfy5Gt+gXTa86i3I/i0iIV9P4hntcGM5GyO+FhZAhxtyg==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/passport": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-10.0.3.tgz", + "integrity": "sha512-znJ9Y4S8ZDVY+j4doWAJ8EuuVO7SkQN3yOBmzxbGaXbvcSwFDAdGJ+OMCg52NdzIO4tQoN4pYKx8W6M0ArfFRQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "passport": "^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0" + } + }, + "node_modules/@nestjs/platform-express": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.22.tgz", + "integrity": "sha512-ySSq7Py/DFozzZdNDH67m/vHoeVdphDniWBnl6q5QVoXldDdrZIHLXLRMPayTDh5A95nt7jjJzmD4qpTbNQ6tA==", + "license": "MIT", + "peer": true, + "dependencies": { + "body-parser": "1.20.4", + "cors": "2.8.5", + "express": "4.22.1", + "multer": "2.0.2", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0" + } + }, + "node_modules/@nestjs/schedule": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-4.1.2.tgz", + "integrity": "sha512-hCTQ1lNjIA5EHxeu8VvQu2Ed2DBLS1GSC6uKPYlBiQe6LL9a7zfE9iVSK+zuK8E2odsApteEBmfAQchc8Hx0Gg==", + "license": "MIT", + "dependencies": { + "cron": "3.2.1", + "uuid": "11.0.3" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/@nestjs/schedule/node_modules/uuid": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz", + "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/@nestjs/schematics": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.2.3.tgz", + "integrity": "sha512-4e8gxaCk7DhBxVUly2PjYL4xC2ifDFexCqq1/u4TtivLGXotVk0wHdYuPYe1tHTHuR1lsOkRbfOCpkdTnigLVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "@angular-devkit/schematics": "17.3.11", + "comment-json": "4.2.5", + "jsonc-parser": "3.3.1", + "pluralize": "8.0.0" + }, + "peerDependencies": { + "typescript": ">=4.8.2" + } + }, + "node_modules/@nestjs/schematics/node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nestjs/swagger": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.4.2.tgz", + "integrity": "sha512-Mu6TEn1M/owIvAx2B4DUQObQXqo2028R2s9rSZ/hJEgBK95+doTwS0DjmVA2wTeZTyVtXOoN7CsoM5pONBzvKQ==", + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "^0.15.0", + "@nestjs/mapped-types": "2.0.5", + "js-yaml": "4.1.0", + "lodash": "4.17.21", + "path-to-regexp": "3.3.0", + "swagger-ui-dist": "5.17.14" + }, + "peerDependencies": { + "@fastify/static": "^6.0.0 || ^7.0.0", + "@nestjs/common": "^9.0.0 || ^10.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0", + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/testing": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.22.tgz", + "integrity": "sha512-HO9aPus3bAedAC+jKVAA8jTdaj4fs5M9fing4giHrcYV2txe9CvC1l1WAjwQ9RDhEHdugjY4y+FZA/U/YqPZrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + } + } + }, + "node_modules/@nestjs/throttler": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@nestjs/throttler/-/throttler-6.5.0.tgz", + "integrity": "sha512-9j0ZRfH0QE1qyrj9JjIRDz5gQLPqq9yVC2nHsrosDVAfI5HHw08/aUAWx9DZLSdQf4HDkmhTTEGLrRFHENvchQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", + "@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", + "reflect-metadata": "^0.1.13 || ^0.2.0" + } + }, + "node_modules/@nuxtjs/opencollective": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz", + "integrity": "sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "consola": "^2.15.0", + "node-fetch": "^2.6.1" + }, + "bin": { + "opencollective": "bin/opencollective.js" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/@otplib/core": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@otplib/core/-/core-12.0.1.tgz", + "integrity": "sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA==", + "license": "MIT" + }, + "node_modules/@otplib/plugin-crypto": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@otplib/plugin-crypto/-/plugin-crypto-12.0.1.tgz", + "integrity": "sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g==", + "deprecated": "Please upgrade to v13 of otplib. Refer to otplib docs for migration paths", + "license": "MIT", + "dependencies": { + "@otplib/core": "^12.0.1" + } + }, + "node_modules/@otplib/plugin-thirty-two": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@otplib/plugin-thirty-two/-/plugin-thirty-two-12.0.1.tgz", + "integrity": "sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA==", + "deprecated": "Please upgrade to v13 of otplib. Refer to otplib docs for migration paths", + "license": "MIT", + "dependencies": { + "@otplib/core": "^12.0.1", + "thirty-two": "^1.0.2" + } + }, + "node_modules/@otplib/preset-default": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@otplib/preset-default/-/preset-default-12.0.1.tgz", + "integrity": "sha512-xf1v9oOJRyXfluBhMdpOkr+bsE+Irt+0D5uHtvg6x1eosfmHCsCC6ej/m7FXiWqdo0+ZUI6xSKDhJwc8yfiOPQ==", + "deprecated": "Please upgrade to v13 of otplib. Refer to otplib docs for migration paths", + "license": "MIT", + "dependencies": { + "@otplib/core": "^12.0.1", + "@otplib/plugin-crypto": "^12.0.1", + "@otplib/plugin-thirty-two": "^12.0.1" + } + }, + "node_modules/@otplib/preset-v11": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@otplib/preset-v11/-/preset-v11-12.0.1.tgz", + "integrity": "sha512-9hSetMI7ECqbFiKICrNa4w70deTUfArtwXykPUvSHWOdzOlfa9ajglu7mNCntlvxycTiOAXkQGwjQCzzDEMRMg==", + "license": "MIT", + "dependencies": { + "@otplib/core": "^12.0.1", + "@otplib/plugin-crypto": "^12.0.1", + "@otplib/plugin-thirty-two": "^12.0.1" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@prisma/client": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.2.tgz", + "integrity": "sha512-gR2EMvfK/aTxsuooaDA32D8v+us/8AAet+C3J1cc04SW35FPdZYgLF+iN4NDLUgAaUGTKdAB0CYenu1TAgGdMg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.2.tgz", + "integrity": "sha512-kadBGDl+aUswv/zZMk9Mx0C8UZs1kjao8H9/JpI4Wh4SHZaM7zkTwiKn/iFLfRg+XtOAo/Z/c6pAYhijKl0nzQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.18.4", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.2.tgz", + "integrity": "sha512-lFnEZsLdFLmEVCVNdskLDCL8Uup41GDfU0LUfquw+ercJC8ODTuL0WNKgOKmYxCJVvFwf0OuZBzW99DuWmoH2A==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.2.tgz", + "integrity": "sha512-TTkJ8r+uk/uqczX40wb+ODG0E0icVsMgwCTyTHXehaEfb0uo80M9g1aW1tEJrxmFHeOZFXdI2sTA1j1AgcHi4A==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.2", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/fetch-engine": "6.19.2", + "@prisma/get-platform": "6.19.2" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz", + "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.2.tgz", + "integrity": "sha512-h4Ff4Pho+SR1S8XerMCC12X//oY2bG3Iug/fUnudfcXEUnIeRiBdXHFdGlGOgQ3HqKgosTEhkZMvGM9tWtYC+Q==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.2", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/get-platform": "6.19.2" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.2.tgz", + "integrity": "sha512-PGLr06JUSTqIvztJtAzIxOwtWKtJm5WwOG6xpsgD37Rc84FpfUBGLKz65YpJBGtkRQGXTYEFie7pYALocC3MtA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.2" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tokenizer/inflate": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", + "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "fflate": "^0.8.2", + "token-types": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie-parser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz", + "integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", + "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/luxon": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", + "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/passport": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", + "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-strategy": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", + "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", + "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/type-utils": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", + "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", + "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.56.1", + "@typescript-eslint/types": "^8.56.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", + "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", + "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", + "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", + "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", + "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.56.1", + "@typescript-eslint/tsconfig-utils": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", + "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", + "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/c12/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/c12/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/c12/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001777", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", + "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/citty/node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT", + "peer": true + }, + "node_modules/class-validator": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.4.tgz", + "integrity": "sha512-AwNusCCam51q703dW82x95tOqQp6oC9HNUl724KxJJOfnKscI8dOloXFgyez7LbTTKWuRBA37FScqVbJEoq8Yw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/validator": "^13.15.3", + "libphonenumber-js": "^1.11.1", + "validator": "^13.15.22" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/comment-json": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.5.tgz", + "integrity": "sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-timsort": "^1.0.3", + "core-util-is": "^1.0.3", + "esprima": "^4.0.1", + "has-own-prop": "^2.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", + "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", + "license": "MIT" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cron": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/cron/-/cron-3.2.1.tgz", + "integrity": "sha512-w2n5l49GMmmkBFEsH9FIDhjZ1n1QgTMOCMGuQtOXs5veNiosZmso6bQGuqOJSYAXXrG84WQFVneNk+Yt0Ua9iw==", + "license": "MIT", + "dependencies": { + "@types/luxon": "~3.4.0", + "luxon": "~3.5.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", + "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/effect": { + "version": "3.18.4", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz", + "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.307", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", + "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz", + "integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.12" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/express/node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/file-type": { + "version": "20.4.1", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.4.1.tgz", + "integrity": "sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.2.6", + "strtok3": "^10.2.0", + "token-types": "^6.0.0", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.4.tgz", + "integrity": "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.0.2.tgz", + "integrity": "sha512-Uochze2R8peoN1XqlSi/rGUkDQpRogtLFocP9+PGu68zk1BDAKXfdeCdyVZpgTk8V8WFVQXdEz426VKjXLO1Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "cosmiconfig": "^8.2.0", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "node-abort-controller": "^3.0.1", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">=12.13.0", + "yarn": ">=1.0.0" + }, + "peerDependencies": { + "typescript": ">3.6.0", + "webpack": "^5.11.0" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/fs-monkey": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gauge/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/giget/node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-own-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz", + "integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/inquirer": { + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", + "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^6.0.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/ioredis": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.0.tgz", + "integrity": "sha512-HVBe9OFuqs+Z6n64q09PQvP1/R4Bm+30PAyyD4wIEqssh3v9L21QjCVk4kRLucMBcDokJTcLjsGeVRlq/nH6DA==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.5.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "license": "ISC", + "engines": { + "node": ">=6" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-config/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jest-config/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-config/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-runtime/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-runtime/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "devOptional": true, + "license": "MIT", + "peer": true, + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.2", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/libphonenumber-js": { + "version": "1.12.38", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.38.tgz", + "integrity": "sha512-vwzxmasAy9hZigxtqTbFEwp8ZdZ975TiqVDwj5bKx5sR+zi5ucUQy9mbVTkKM9GzqdLdxux/hTw2nmN5J7POMA==", + "license": "MIT" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/luxon": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/magic-string": { + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", + "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/nypm": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz", + "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.2.0", + "pathe": "^2.0.3", + "tinyexec": "^1.0.2" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.1.tgz", + "integrity": "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/otplib": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/otplib/-/otplib-12.0.1.tgz", + "integrity": "sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==", + "license": "MIT", + "dependencies": { + "@otplib/core": "^12.0.1", + "@otplib/preset-default": "^12.0.1", + "@otplib/preset-v11": "^12.0.1" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "license": "MIT", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", + "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prisma": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.2.tgz", + "integrity": "sha512-XTKeKxtQElcq3U9/jHyxSPgiRgeYDKxWTPOf6NkXA0dNj5j40MfEsZkMbyNpwDWCUv7YBFUl7I2VK/6ALbmhEg==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@prisma/config": "6.19.2", + "@prisma/engines": "6.19.2" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qrcode/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/qrcode/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/qrcode/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/qrcode/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strtok3": { + "version": "10.3.4", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", + "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.17.14", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz", + "integrity": "sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw==", + "license": "Apache-2.0" + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/synckit": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/terser": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.17", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.17.tgz", + "integrity": "sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/thirty-two": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/thirty-two/-/thirty-two-1.0.2.tgz", + "integrity": "sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==", + "engines": { + "node": ">=0.2.6" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-jest": { + "version": "29.4.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", + "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.3", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-loader": { + "version": "9.5.4", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.4.tgz", + "integrity": "sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tapable": "^2.2.1", + "tsconfig-paths": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "license": "MIT", + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/validator": { + "version": "13.15.26", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz", + "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/webpack": { + "version": "5.105.4", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz", + "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.20.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.17", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-node-externals": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", + "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/packages/core-service/package.json b/packages/core-service/package.json index 43517c4..cadfd37 100644 --- a/packages/core-service/package.json +++ b/packages/core-service/package.json @@ -22,7 +22,8 @@ "prisma:generate": "prisma generate --schema=prisma/core.schema.prisma", "prisma:migrate": "prisma migrate dev --schema=prisma/core.schema.prisma", "prisma:migrate:deploy": "prisma migrate deploy --schema=prisma/core.schema.prisma", - "prisma:studio": "prisma studio --schema=prisma/core.schema.prisma" + "prisma:studio": "prisma studio --schema=prisma/core.schema.prisma", + "prisma:seed": "ts-node prisma/seed.ts" }, "dependencies": { "@nestjs/common": "^10.4.0", diff --git a/packages/core-service/prisma/migrations/20260308000000_init/migration.sql b/packages/core-service/prisma/migrations/20260308000000_init/migration.sql new file mode 100644 index 0000000..ec06b93 --- /dev/null +++ b/packages/core-service/prisma/migrations/20260308000000_init/migration.sql @@ -0,0 +1,146 @@ +-- CreateTable +CREATE TABLE "users" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "email" VARCHAR(255) NOT NULL, + "first_name" VARCHAR(100) NOT NULL, + "last_name" VARCHAR(100) NOT NULL, + "role" VARCHAR(50) NOT NULL DEFAULT 'USER', + "is_active" BOOLEAN NOT NULL DEFAULT true, + "two_factor_enabled" BOOLEAN NOT NULL DEFAULT false, + "last_login" TIMESTAMP(3), + "failed_login_attempts" INTEGER NOT NULL DEFAULT 0, + "last_failed_login" TIMESTAMP(3), + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "users_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "auth_providers" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "user_id" UUID NOT NULL, + "provider" VARCHAR(50) NOT NULL, + "provider_id" VARCHAR(255), + "password_hash" VARCHAR(255), + "totp_secret" VARCHAR(255), + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "auth_providers_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "tenants" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "name" VARCHAR(200) NOT NULL, + "slug" VARCHAR(50) NOT NULL, + "is_active" BOOLEAN NOT NULL DEFAULT true, + "settings" JSONB NOT NULL DEFAULT '{}', + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "tenants_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "tenant_memberships" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "user_id" UUID NOT NULL, + "tenant_id" UUID NOT NULL, + "tenant_role" VARCHAR(50) NOT NULL DEFAULT 'MEMBER', + "is_active" BOOLEAN NOT NULL DEFAULT true, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "tenant_memberships_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "modules" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "key" VARCHAR(50) NOT NULL, + "name" VARCHAR(100) NOT NULL, + "description" TEXT, + "version" VARCHAR(20) NOT NULL DEFAULT '1.0.0', + "is_active" BOOLEAN NOT NULL DEFAULT true, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "modules_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "tenant_modules" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "tenant_id" UUID NOT NULL, + "module_id" UUID NOT NULL, + "is_active" BOOLEAN NOT NULL DEFAULT true, + "config" JSONB NOT NULL DEFAULT '{}', + "activated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "tenant_modules_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "audit_logs" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "user_id" UUID, + "action" VARCHAR(100) NOT NULL, + "entity" VARCHAR(100) NOT NULL, + "entity_id" VARCHAR(255), + "details" JSONB, + "ip_address" VARCHAR(45), + "user_agent" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "audit_logs_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "auth_providers_user_id_provider_key" ON "auth_providers"("user_id", "provider"); + +-- CreateIndex +CREATE UNIQUE INDEX "tenants_slug_key" ON "tenants"("slug"); + +-- CreateIndex +CREATE UNIQUE INDEX "tenant_memberships_user_id_tenant_id_key" ON "tenant_memberships"("user_id", "tenant_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "modules_key_key" ON "modules"("key"); + +-- CreateIndex +CREATE UNIQUE INDEX "tenant_modules_tenant_id_module_id_key" ON "tenant_modules"("tenant_id", "module_id"); + +-- CreateIndex +CREATE INDEX "audit_logs_user_id_idx" ON "audit_logs"("user_id"); + +-- CreateIndex +CREATE INDEX "audit_logs_action_idx" ON "audit_logs"("action"); + +-- CreateIndex +CREATE INDEX "audit_logs_entity_entity_id_idx" ON "audit_logs"("entity", "entity_id"); + +-- CreateIndex +CREATE INDEX "audit_logs_created_at_idx" ON "audit_logs"("created_at"); + +-- AddForeignKey +ALTER TABLE "auth_providers" ADD CONSTRAINT "auth_providers_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "tenant_memberships" ADD CONSTRAINT "tenant_memberships_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "tenant_memberships" ADD CONSTRAINT "tenant_memberships_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "tenant_modules" ADD CONSTRAINT "tenant_modules_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "tenant_modules" ADD CONSTRAINT "tenant_modules_module_id_fkey" FOREIGN KEY ("module_id") REFERENCES "modules"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "audit_logs" ADD CONSTRAINT "audit_logs_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/packages/core-service/prisma/migrations/migration_lock.toml b/packages/core-service/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..99e4f20 --- /dev/null +++ b/packages/core-service/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" diff --git a/packages/core-service/prisma/seed.ts b/packages/core-service/prisma/seed.ts new file mode 100644 index 0000000..143768e --- /dev/null +++ b/packages/core-service/prisma/seed.ts @@ -0,0 +1,65 @@ +import { PrismaClient } from '@prisma/client'; +import * as bcrypt from 'bcrypt'; + +/** + * Seed-Script: Erstellt den initialen Platform-Admin User. + * + * Ausfuehrung: + * npx ts-node prisma/seed.ts + * + * WICHTIG: Passwort nach erstem Login aendern! + */ + +const prisma = new PrismaClient({ + datasources: { + db: { + url: process.env.DATABASE_URL_DIRECT || process.env.DATABASE_URL, + }, + }, +}); + +async function main(): Promise { + const email = 'admin@xinion.de'; + + // Pruefen ob Admin bereits existiert + const existing = await prisma.user.findUnique({ where: { email } }); + if (existing) { + console.log(`Admin-User ${email} existiert bereits. Seed uebersprungen.`); + return; + } + + // Passwort hashen (Bcrypt Cost 12 gemaess Sicherheitsregel) + const passwordHash = await bcrypt.hash('ChangeMe123!', 12); + + // Admin-User anlegen + const user = await prisma.user.create({ + data: { + email, + firstName: 'Platform', + lastName: 'Admin', + role: 'PLATFORM_ADMIN', + isActive: true, + authProvider: { + create: { + provider: 'LOCAL', + passwordHash, + }, + }, + }, + }); + + console.log(`Platform-Admin erstellt: ${user.email} (ID: ${user.id})`); + console.log(''); + console.log('Zugangsdaten:'); + console.log(` E-Mail: ${email}`); + console.log(' Passwort: ChangeMe123!'); + console.log(''); + console.log('WICHTIG: Passwort nach erstem Login aendern!'); +} + +main() + .catch((e: Error) => { + console.error('Seed fehlgeschlagen:', e.message); + process.exit(1); + }) + .finally(() => prisma.$disconnect()); diff --git a/packages/core-service/src/config/env.validation.ts b/packages/core-service/src/config/env.validation.ts index 65dc142..35737fc 100644 --- a/packages/core-service/src/config/env.validation.ts +++ b/packages/core-service/src/config/env.validation.ts @@ -27,7 +27,7 @@ class EnvironmentVariables { @IsString() @IsNotEmpty() - APP_URL = 'https://insight-dev.xinion.lan'; + APP_URL = 'http://172.20.10.59'; // Datenbank @IsString() diff --git a/packages/core-service/src/core/auth/auth.controller.ts b/packages/core-service/src/core/auth/auth.controller.ts index a71493d..bcb4d35 100644 --- a/packages/core-service/src/core/auth/auth.controller.ts +++ b/packages/core-service/src/core/auth/auth.controller.ts @@ -96,10 +96,11 @@ export class AuthController { await this.authService.logout(user, refreshToken); // Refresh-Token Cookie loeschen + const isProduction = process.env.NODE_ENV === 'production'; res.clearCookie('refresh_token', { httpOnly: true, - secure: true, - sameSite: 'strict', + secure: isProduction, + sameSite: isProduction ? 'strict' : 'lax', path: '/api/v1/auth', }); @@ -107,14 +108,17 @@ export class AuthController { } /** - * Setzt das Refresh-Token als HttpOnly, Secure, SameSite=Strict Cookie. + * Setzt das Refresh-Token als HttpOnly Cookie. + * Secure + SameSite=Strict nur in Produktion (HTTPS). + * In Development (HTTP) wird Secure deaktiviert und SameSite=Lax gesetzt. */ private setRefreshTokenCookie(res: Response, refreshToken: string): void { + const isProduction = process.env.NODE_ENV === 'production'; res.cookie('refresh_token', refreshToken, { httpOnly: true, - secure: true, // Nur HTTPS - sameSite: 'strict', - path: '/api/v1/auth', // Nur fuer Auth-Endpunkte + secure: isProduction, + sameSite: isProduction ? 'strict' : 'lax', + path: '/api/v1/auth', maxAge: 7 * 24 * 60 * 60 * 1000, // 7 Tage }); } diff --git a/packages/core-service/src/main.ts b/packages/core-service/src/main.ts index 3f2a509..8af8139 100644 --- a/packages/core-service/src/main.ts +++ b/packages/core-service/src/main.ts @@ -17,7 +17,7 @@ async function bootstrap(): Promise { // CORS const corsOrigins = process.env.CORS_ORIGINS?.split(',') ?? [ - 'https://insight-dev.xinion.lan', + 'http://172.20.10.59', ]; app.enableCors({ origin: corsOrigins, diff --git a/packages/frontend/package-lock.json b/packages/frontend/package-lock.json new file mode 100644 index 0000000..d3f8e30 --- /dev/null +++ b/packages/frontend/package-lock.json @@ -0,0 +1,3664 @@ +{ + "name": "@insight/frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@insight/frontend", + "version": "0.1.0", + "dependencies": { + "@tanstack/react-query": "^5.56.0", + "axios": "^1.7.0", + "react": "^18.3.0", + "react-dom": "^18.3.0", + "react-router-dom": "^6.26.0" + }, + "devDependencies": { + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", + "@vitejs/plugin-react": "^4.3.0", + "eslint": "^9.0.0", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.0", + "typescript": "^5.6.0", + "vite": "^6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.21", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", + "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", + "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/type-utils": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", + "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", + "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.56.1", + "@typescript-eslint/types": "^8.56.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", + "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", + "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", + "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", + "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", + "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.56.1", + "@typescript-eslint/tsconfig-utils": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", + "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", + "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001777", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", + "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.307", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", + "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.4.tgz", + "integrity": "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} From 8660966c9e4a4b06eb57aa8afb8cabb66b574eeb Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Sun, 8 Mar 2026 16:38:47 +0100 Subject: [PATCH 07/50] fix: correct PgBouncer port to 5432 (image default) The edoburu/pgbouncer image listens on port 5432 internally, not 6432. Updated healthcheck and DATABASE_URL accordingly. Co-Authored-By: Claude Opus 4.6 --- .env.example | 2 +- docker-compose.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 41c54f6..2e6e0f1 100644 --- a/.env.example +++ b/.env.example @@ -14,7 +14,7 @@ LOG_LEVEL=info # --- PostgreSQL --- DB_HOST=pgbouncer -DB_PORT=6432 +DB_PORT=5432 DB_USER=insight DB_PASSWORD= # Sicheres Passwort setzen! DB_NAME=platform_core diff --git a/docker-compose.yml b/docker-compose.yml index 2fc08db..df3a86f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -149,7 +149,7 @@ services: postgres: condition: service_healthy healthcheck: - test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -p 6432"] + test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -p 5432"] interval: 10s timeout: 5s retries: 3 @@ -222,7 +222,7 @@ services: FRONTEND_URL: ${FRONTEND_URL:-http://172.20.10.59} LOG_LEVEL: ${LOG_LEVEL:-info} # Database (via PgBouncer) - DATABASE_URL: "postgresql://${DB_USER:-insight}:${DB_PASSWORD}@pgbouncer:6432/${DB_NAME:-platform_core}" + DATABASE_URL: "postgresql://${DB_USER:-insight}:${DB_PASSWORD}@pgbouncer:5432/${DB_NAME:-platform_core}" # Database (direkt fuer Migrations) DATABASE_URL_DIRECT: "postgresql://${DB_USER:-insight}:${DB_PASSWORD}@postgres:5432/${DB_NAME:-platform_core}" # Redis From b2ef16eb28c758b556cfd405ed8b720c255bde92 Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Sun, 8 Mar 2026 16:42:06 +0100 Subject: [PATCH 08/50] fix: add bcrypt native module rebuild to Dockerfile bcrypt requires native compilation which was skipped by --ignore-scripts. Added python3/make/g++ and npm rebuild bcrypt. Co-Authored-By: Claude Opus 4.6 --- packages/core-service/Dockerfile | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/core-service/Dockerfile b/packages/core-service/Dockerfile index 83f288c..918ea57 100644 --- a/packages/core-service/Dockerfile +++ b/packages/core-service/Dockerfile @@ -9,8 +9,12 @@ RUN apk add --no-cache openssl # --- Dependencies Stage --- FROM base AS deps +# Build-Tools fuer native Module (bcrypt) +RUN apk add --no-cache python3 make g++ COPY package.json package-lock.json* ./ RUN npm ci --ignore-scripts +# Native Module kompilieren (bcrypt) +RUN npm rebuild bcrypt # Prisma Generate braucht die Schema-Dateien COPY prisma ./prisma RUN npx prisma generate --schema=prisma/core.schema.prisma @@ -36,9 +40,16 @@ FROM base AS production WORKDIR /app ENV NODE_ENV=production +# Build-Tools fuer native Module (bcrypt) +RUN apk add --no-cache python3 make g++ + # Nur Produktions-Dependencies COPY package.json package-lock.json* ./ RUN npm ci --omit=dev --ignore-scripts +RUN npm rebuild bcrypt + +# Build-Tools entfernen (Image klein halten) +RUN apk del python3 make g++ # Prisma Client generieren COPY prisma ./prisma From 5214d5e0f79a151dfa8f0de29f297fb8fa0c878f Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Sun, 8 Mar 2026 16:44:45 +0100 Subject: [PATCH 09/50] fix: resolve TypeScript compilation errors - Fix cookieParser import (default import instead of namespace) - Cast tenant settings to Prisma.InputJsonValue for type safety Co-Authored-By: Claude Opus 4.6 --- packages/core-service/src/core/tenants/tenants.service.ts | 5 +++-- packages/core-service/src/main.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/core-service/src/core/tenants/tenants.service.ts b/packages/core-service/src/core/tenants/tenants.service.ts index 9dff481..192b5f0 100644 --- a/packages/core-service/src/core/tenants/tenants.service.ts +++ b/packages/core-service/src/core/tenants/tenants.service.ts @@ -4,6 +4,7 @@ import { NotFoundException, Logger, } from '@nestjs/common'; +import { Prisma } from '@prisma/client'; import { PrismaService } from '../../prisma/prisma.service'; import { CreateTenantDto } from './dto/create-tenant.dto'; import { UpdateTenantDto } from './dto/update-tenant.dto'; @@ -31,7 +32,7 @@ export class TenantsService { name: dto.name, slug: dto.slug, isActive: true, - settings: dto.settings ?? {}, + settings: (dto.settings ?? {}) as Prisma.InputJsonValue, }, }); @@ -118,7 +119,7 @@ export class TenantsService { data: { name: dto.name, isActive: dto.isActive, - settings: dto.settings, + settings: dto.settings as Prisma.InputJsonValue, }, }); } diff --git a/packages/core-service/src/main.ts b/packages/core-service/src/main.ts index 8af8139..ae0050f 100644 --- a/packages/core-service/src/main.ts +++ b/packages/core-service/src/main.ts @@ -1,7 +1,7 @@ import { NestFactory } from '@nestjs/core'; import { ValidationPipe, Logger } from '@nestjs/common'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; -import * as cookieParser from 'cookie-parser'; +import cookieParser from 'cookie-parser'; import helmet from 'helmet'; import { AppModule } from './app.module'; From 2059fa69d940f15d2a3e9b64a03c3163801d5f77 Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Sun, 8 Mar 2026 16:46:06 +0100 Subject: [PATCH 10/50] fix: add @Type(() => Number) for numeric env var conversion Environment variables are strings from process.env. Explicit Type decorators ensure class-transformer converts them to numbers. Co-Authored-By: Claude Opus 4.6 --- packages/core-service/src/config/env.validation.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/core-service/src/config/env.validation.ts b/packages/core-service/src/config/env.validation.ts index 35737fc..ce8fd54 100644 --- a/packages/core-service/src/config/env.validation.ts +++ b/packages/core-service/src/config/env.validation.ts @@ -1,4 +1,4 @@ -import { plainToInstance } from 'class-transformer'; +import { plainToInstance, Type } from 'class-transformer'; import { IsEnum, IsNotEmpty, @@ -20,6 +20,7 @@ class EnvironmentVariables { @IsEnum(Environment) NODE_ENV: Environment = Environment.Development; + @Type(() => Number) @IsNumber() @Min(1) @Max(65535) @@ -42,6 +43,7 @@ class EnvironmentVariables { @IsString() REDIS_HOST = 'redis'; + @Type(() => Number) @IsNumber() REDIS_PORT = 6379; @@ -68,15 +70,18 @@ class EnvironmentVariables { JWT_ISSUER = 'insight-platform'; // Bcrypt + @Type(() => Number) @IsNumber() @Min(10) @Max(14) BCRYPT_COST = 12; // Rate Limiting + @Type(() => Number) @IsNumber() THROTTLE_TTL = 60000; + @Type(() => Number) @IsNumber() THROTTLE_LIMIT = 200; } From 5f567445be73fd19493c637c8a4a686308cb3f3c Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Sun, 8 Mar 2026 17:43:37 +0100 Subject: [PATCH 11/50] fix: use wget instead of curl for healthchecks in Alpine containers Alpine images don't include curl. wget is available by default. Co-Authored-By: Claude Opus 4.6 --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index df3a86f..c26f3f5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -269,7 +269,7 @@ services: - "traefik.http.middlewares.api-ratelimit.ratelimit.burst=50" - "traefik.http.routers.core-api.middlewares=api-ratelimit" healthcheck: - test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"] + test: ["CMD-SHELL", "wget -qO- http://localhost:3000/health || exit 1"] interval: 15s timeout: 5s retries: 3 @@ -296,7 +296,7 @@ services: - "traefik.http.routers.frontend.priority=1" - "traefik.http.services.frontend.loadbalancer.server.port=8080" healthcheck: - test: ["CMD-SHELL", "curl -f http://localhost:8080/ || exit 1"] + test: ["CMD-SHELL", "wget -qO- http://localhost:8080/ || exit 1"] interval: 30s timeout: 5s retries: 3 From f337851bb987f729d6687b9a6cf7a1196c0bd568 Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Sun, 8 Mar 2026 18:07:06 +0100 Subject: [PATCH 12/50] fix: enable Traefik ping for healthcheck The `traefik healthcheck` command requires `--ping=true` to be set. Without it, the healthcheck always fails even though Traefik works fine. Co-Authored-By: Claude Opus 4.6 --- docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index c26f3f5..c76ea62 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -58,6 +58,8 @@ services: - "--log.level=INFO" - "--accesslog=true" - "--accesslog.format=json" + # Ping (fuer Healthcheck) + - "--ping=true" # Metrics fuer Prometheus - "--metrics.prometheus=true" - "--metrics.prometheus.entryPoint=metrics" From 4d5d5bac8880991ac671faa9852ce08f5dff9110 Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Sun, 8 Mar 2026 18:11:32 +0100 Subject: [PATCH 13/50] fix: use wget-based healthcheck for Traefik The `traefik healthcheck` CLI command doesn't reliably detect the ping configuration. Using wget against the /ping endpoint instead. Co-Authored-By: Claude Opus 4.6 --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index c76ea62..5b8288c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -80,7 +80,7 @@ services: - "traefik.http.routers.dashboard.service=api@internal" - "traefik.http.routers.dashboard.entrypoints=web" healthcheck: - test: ["CMD", "traefik", "healthcheck"] + test: ["CMD-SHELL", "wget -qO- http://localhost:8080/ping || exit 1"] interval: 30s timeout: 5s retries: 3 From bd00ea5e50efc481188c60303738c453a1deaf5a Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Sun, 8 Mar 2026 18:21:17 +0100 Subject: [PATCH 14/50] fix: convert query params to numbers for pagination @Query() decorator always returns strings. Using Number() conversion with fallback to defaults (page=1, limit=20) to prevent NaN errors in Prisma findMany skip/take calculations. Co-Authored-By: Claude Opus 4.6 --- .../core-service/src/core/tenants/tenants.controller.ts | 9 ++++++--- packages/core-service/src/core/users/users.controller.ts | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/core-service/src/core/tenants/tenants.controller.ts b/packages/core-service/src/core/tenants/tenants.controller.ts index bb77761..6566244 100644 --- a/packages/core-service/src/core/tenants/tenants.controller.ts +++ b/packages/core-service/src/core/tenants/tenants.controller.ts @@ -33,10 +33,13 @@ export class TenantsController { @UseGuards(RolesGuard) @ApiOperation({ summary: 'Alle Mandanten auflisten (Admin)' }) async findAll( - @Query('page') page?: number, - @Query('limit') limit?: number, + @Query('page') page?: string, + @Query('limit') limit?: string, ) { - return this.tenantsService.findAll(page ?? 1, limit ?? 20); + return this.tenantsService.findAll( + Number(page) || 1, + Number(limit) || 20, + ); } /** diff --git a/packages/core-service/src/core/users/users.controller.ts b/packages/core-service/src/core/users/users.controller.ts index 83b1da8..1552383 100644 --- a/packages/core-service/src/core/users/users.controller.ts +++ b/packages/core-service/src/core/users/users.controller.ts @@ -42,10 +42,13 @@ export class UsersController { @UseGuards(RolesGuard) @ApiOperation({ summary: 'Alle Benutzer auflisten (Admin)' }) async findAll( - @Query('page') page?: number, - @Query('limit') limit?: number, + @Query('page') page?: string, + @Query('limit') limit?: string, ) { - return this.usersService.findAll(page ?? 1, limit ?? 20); + return this.usersService.findAll( + Number(page) || 1, + Number(limit) || 20, + ); } /** From 779d90ca43b257b25ec89b30f0a62cc81f20c380 Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Sun, 8 Mar 2026 20:24:44 +0100 Subject: [PATCH 15/50] feat: add user profile page with 2FA management and password change Backend: - POST /auth/2fa/setup - generate TOTP secret + QR code (temp Redis storage) - POST /auth/2fa/enable - verify TOTP code and activate 2FA - POST /auth/2fa/disable - deactivate 2FA (requires password) - PATCH /users/me - update own profile (firstName, lastName) - POST /users/me/change-password - change own password Frontend: - New ProfilePage with 3 sections: personal info, password, 2FA - QR code display for Authenticator app setup - Clickable user info in sidebar navigates to /profile - AuthContext extended with twoFactorEnabled + refreshUser Co-Authored-By: Claude Opus 4.6 --- .../src/core/auth/auth.controller.ts | 46 ++ .../src/core/auth/auth.service.ts | 108 +++++ .../src/core/auth/dto/disable-2fa.dto.ts | 13 + .../src/core/auth/dto/enable-2fa.dto.ts | 13 + .../src/core/users/dto/change-password.dto.ts | 25 + .../src/core/users/users.controller.ts | 35 ++ .../src/core/users/users.service.ts | 73 +++ packages/frontend/src/auth/AuthContext.tsx | 12 + .../src/profile/ProfilePage.module.css | 206 ++++++++ packages/frontend/src/profile/ProfilePage.tsx | 452 ++++++++++++++++++ packages/frontend/src/shell/App.tsx | 2 + .../frontend/src/shell/AppLayout.module.css | 12 + packages/frontend/src/shell/AppLayout.tsx | 16 +- 13 files changed, 1010 insertions(+), 3 deletions(-) create mode 100644 packages/core-service/src/core/auth/dto/disable-2fa.dto.ts create mode 100644 packages/core-service/src/core/auth/dto/enable-2fa.dto.ts create mode 100644 packages/core-service/src/core/users/dto/change-password.dto.ts create mode 100644 packages/frontend/src/profile/ProfilePage.module.css create mode 100644 packages/frontend/src/profile/ProfilePage.tsx diff --git a/packages/core-service/src/core/auth/auth.controller.ts b/packages/core-service/src/core/auth/auth.controller.ts index bcb4d35..019bd4f 100644 --- a/packages/core-service/src/core/auth/auth.controller.ts +++ b/packages/core-service/src/core/auth/auth.controller.ts @@ -13,6 +13,8 @@ import { Public } from '../../common/decorators/public.decorator'; import { CurrentUser, JwtPayload } from '../../common/decorators/current-user.decorator'; import { AuthService } from './auth.service'; import { LoginDto } from './dto/login.dto'; +import { Enable2faDto } from './dto/enable-2fa.dto'; +import { Disable2faDto } from './dto/disable-2fa.dto'; @ApiTags('Authentifizierung') @Controller('auth') @@ -107,6 +109,50 @@ export class AuthController { return { message: 'Erfolgreich abgemeldet' }; } + /** + * POST /api/v1/auth/2fa/setup + * 2FA-Setup starten: Secret + QR-Code generieren. + */ + @Post('2fa/setup') + @HttpCode(HttpStatus.OK) + @ApiBearerAuth('access-token') + @ApiOperation({ summary: '2FA-Setup starten (QR-Code generieren)' }) + async setup2fa(@CurrentUser('sub') userId: string) { + return this.authService.setup2fa(userId); + } + + /** + * POST /api/v1/auth/2fa/enable + * 2FA aktivieren: TOTP-Code verifizieren. + */ + @Post('2fa/enable') + @HttpCode(HttpStatus.OK) + @ApiBearerAuth('access-token') + @ApiOperation({ summary: '2FA aktivieren (Code verifizieren)' }) + async enable2fa( + @CurrentUser('sub') userId: string, + @Body() dto: Enable2faDto, + ) { + await this.authService.enable2fa(userId, dto.totpCode); + return { message: '2FA wurde erfolgreich aktiviert' }; + } + + /** + * POST /api/v1/auth/2fa/disable + * 2FA deaktivieren (mit Passwort-Bestaetigung). + */ + @Post('2fa/disable') + @HttpCode(HttpStatus.OK) + @ApiBearerAuth('access-token') + @ApiOperation({ summary: '2FA deaktivieren (Passwort erforderlich)' }) + async disable2fa( + @CurrentUser('sub') userId: string, + @Body() dto: Disable2faDto, + ) { + await this.authService.disable2fa(userId, dto.password); + return { message: '2FA wurde erfolgreich deaktiviert' }; + } + /** * Setzt das Refresh-Token als HttpOnly Cookie. * Secure + SameSite=Strict nur in Produktion (HTTPS). diff --git a/packages/core-service/src/core/auth/auth.service.ts b/packages/core-service/src/core/auth/auth.service.ts index 82bd4c1..b0cdc51 100644 --- a/packages/core-service/src/core/auth/auth.service.ts +++ b/packages/core-service/src/core/auth/auth.service.ts @@ -226,6 +226,114 @@ export class AuthService { this.logger.log(`Logout: User ${accessToken.sub}`); } + /** + * 2FA-Setup: Neues TOTP-Secret generieren und QR-Code zurueckgeben. + * Secret wird temporaer in Redis gespeichert (5 Minuten TTL). + * Erst nach Verifizierung wird das Secret permanent in der DB gespeichert. + */ + async setup2fa(userId: string): Promise<{ qrCode: string; secret: string }> { + const user = await this.prisma.user.findUnique({ + where: { id: userId }, + }); + + if (!user) { + throw new UnauthorizedException('Benutzer nicht gefunden'); + } + + if (user.twoFactorEnabled) { + throw new ForbiddenException('2FA ist bereits aktiviert'); + } + + const secret = this.totp.generateSecret(); + const qrCode = await this.totp.generateQrCode(user.email, secret); + + // Secret temporaer in Redis speichern (5 Minuten zum Einrichten) + await this.redis.set(`2fa_setup:${userId}`, secret, 300); + + this.logger.log(`2FA-Setup gestartet fuer User ${user.email}`); + + return { qrCode, secret }; + } + + /** + * 2FA aktivieren: TOTP-Code verifizieren und Secret permanent speichern. + */ + async enable2fa(userId: string, totpCode: string): Promise { + // Temporaeres Secret aus Redis holen + const secret = await this.redis.get(`2fa_setup:${userId}`); + if (!secret) { + throw new ForbiddenException( + '2FA-Setup abgelaufen. Bitte erneut starten.', + ); + } + + // TOTP-Code pruefen + const isValid = this.totp.verify(totpCode, secret); + if (!isValid) { + throw new UnauthorizedException('Ungueltiger 2FA-Code'); + } + + // Secret permanent in AuthProvider speichern + 2FA aktivieren + await this.prisma.$transaction([ + this.prisma.authProvider.updateMany({ + where: { userId, provider: 'LOCAL' }, + data: { totpSecret: secret }, + }), + this.prisma.user.update({ + where: { id: userId }, + data: { twoFactorEnabled: true }, + }), + ]); + + // Temporaeres Secret aus Redis loeschen + await this.redis.del(`2fa_setup:${userId}`); + + this.logger.log(`2FA aktiviert fuer User ${userId}`); + } + + /** + * 2FA deaktivieren: Passwort-Verifikation erforderlich. + */ + async disable2fa(userId: string, password: string): Promise { + const user = await this.prisma.user.findUnique({ + where: { id: userId }, + include: { authProvider: true }, + }); + + if (!user) { + throw new UnauthorizedException('Benutzer nicht gefunden'); + } + + if (!user.twoFactorEnabled) { + throw new ForbiddenException('2FA ist nicht aktiviert'); + } + + // Passwort pruefen + const localAuth = user.authProvider.find((ap) => ap.provider === 'LOCAL'); + if (!localAuth?.passwordHash) { + throw new UnauthorizedException('Kein lokaler Auth-Provider gefunden'); + } + + const passwordValid = await bcrypt.compare(password, localAuth.passwordHash); + if (!passwordValid) { + throw new UnauthorizedException('Ungueltiges Passwort'); + } + + // 2FA deaktivieren + Secret loeschen + await this.prisma.$transaction([ + this.prisma.authProvider.updateMany({ + where: { userId, provider: 'LOCAL' }, + data: { totpSecret: null }, + }), + this.prisma.user.update({ + where: { id: userId }, + data: { twoFactorEnabled: false }, + }), + ]); + + this.logger.log(`2FA deaktiviert fuer User ${userId}`); + } + /** * Token-Paar generieren (Access + Refresh). */ diff --git a/packages/core-service/src/core/auth/dto/disable-2fa.dto.ts b/packages/core-service/src/core/auth/dto/disable-2fa.dto.ts new file mode 100644 index 0000000..9cc1ace --- /dev/null +++ b/packages/core-service/src/core/auth/dto/disable-2fa.dto.ts @@ -0,0 +1,13 @@ +import { IsNotEmpty, IsString, MinLength } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class Disable2faDto { + @ApiProperty({ + example: 'SicheresPasswort123!', + description: 'Aktuelles Passwort zur Bestaetigung', + }) + @IsString() + @IsNotEmpty({ message: 'Passwort darf nicht leer sein' }) + @MinLength(8, { message: 'Passwort muss mindestens 8 Zeichen lang sein' }) + password!: string; +} diff --git a/packages/core-service/src/core/auth/dto/enable-2fa.dto.ts b/packages/core-service/src/core/auth/dto/enable-2fa.dto.ts new file mode 100644 index 0000000..b99021c --- /dev/null +++ b/packages/core-service/src/core/auth/dto/enable-2fa.dto.ts @@ -0,0 +1,13 @@ +import { IsNotEmpty, IsString, Length } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class Enable2faDto { + @ApiProperty({ + example: '123456', + description: 'TOTP-Code aus der Authenticator-App', + }) + @IsString() + @IsNotEmpty({ message: '2FA-Code darf nicht leer sein' }) + @Length(6, 6, { message: '2FA-Code muss genau 6 Zeichen lang sein' }) + totpCode!: string; +} diff --git a/packages/core-service/src/core/users/dto/change-password.dto.ts b/packages/core-service/src/core/users/dto/change-password.dto.ts new file mode 100644 index 0000000..1254d76 --- /dev/null +++ b/packages/core-service/src/core/users/dto/change-password.dto.ts @@ -0,0 +1,25 @@ +import { IsNotEmpty, IsString, MinLength, MaxLength } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class ChangePasswordDto { + @ApiProperty({ + example: 'AltesPasswort123!', + description: 'Aktuelles Passwort', + }) + @IsString() + @IsNotEmpty({ message: 'Aktuelles Passwort darf nicht leer sein' }) + @MinLength(8, { message: 'Passwort muss mindestens 8 Zeichen lang sein' }) + currentPassword!: string; + + @ApiProperty({ + example: 'NeuesSicheresPasswort456!', + description: 'Neues Passwort (mindestens 8 Zeichen)', + }) + @IsString() + @IsNotEmpty({ message: 'Neues Passwort darf nicht leer sein' }) + @MinLength(8, { + message: 'Neues Passwort muss mindestens 8 Zeichen lang sein', + }) + @MaxLength(128, { message: 'Passwort darf maximal 128 Zeichen lang sein' }) + newPassword!: string; +} diff --git a/packages/core-service/src/core/users/users.controller.ts b/packages/core-service/src/core/users/users.controller.ts index 1552383..d8b5990 100644 --- a/packages/core-service/src/core/users/users.controller.ts +++ b/packages/core-service/src/core/users/users.controller.ts @@ -8,11 +8,14 @@ import { Query, UseGuards, ParseUUIDPipe, + HttpCode, + HttpStatus, } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; import { UsersService } from './users.service'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; +import { ChangePasswordDto } from './dto/change-password.dto'; import { Roles } from '../../common/decorators/roles.decorator'; import { CurrentUser, JwtPayload } from '../../common/decorators/current-user.decorator'; import { RolesGuard } from '../../common/guards/roles.guard'; @@ -33,6 +36,38 @@ export class UsersController { return this.usersService.findById(userId); } + /** + * PATCH /api/v1/users/me + * Eigenes Profil aktualisieren (firstName, lastName). + */ + @Patch('me') + @ApiOperation({ summary: 'Eigenes Profil aktualisieren' }) + async updateProfile( + @CurrentUser('sub') userId: string, + @Body() dto: UpdateUserDto, + ) { + return this.usersService.updateProfile(userId, dto); + } + + /** + * POST /api/v1/users/me/change-password + * Eigenes Passwort aendern. + */ + @Post('me/change-password') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Eigenes Passwort aendern' }) + async changePassword( + @CurrentUser('sub') userId: string, + @Body() dto: ChangePasswordDto, + ) { + await this.usersService.changePassword( + userId, + dto.currentPassword, + dto.newPassword, + ); + return { message: 'Passwort erfolgreich geaendert' }; + } + /** * GET /api/v1/users * Alle User auflisten (nur PLATFORM_ADMIN). diff --git a/packages/core-service/src/core/users/users.service.ts b/packages/core-service/src/core/users/users.service.ts index 920ec3e..9b00363 100644 --- a/packages/core-service/src/core/users/users.service.ts +++ b/packages/core-service/src/core/users/users.service.ts @@ -2,6 +2,7 @@ import { Injectable, ConflictException, NotFoundException, + UnauthorizedException, Logger, } from '@nestjs/common'; import * as bcrypt from 'bcrypt'; @@ -136,6 +137,78 @@ export class UsersService { }; } + /** + * Eigenes Profil aktualisieren (nur firstName, lastName). + */ + async updateProfile(userId: string, dto: UpdateUserDto) { + const user = await this.prisma.user.findUnique({ where: { id: userId } }); + if (!user) { + throw new NotFoundException('Benutzer nicht gefunden'); + } + + const updated = await this.prisma.user.update({ + where: { id: userId }, + data: { + ...(dto.firstName !== undefined && { firstName: dto.firstName }), + ...(dto.lastName !== undefined && { lastName: dto.lastName }), + }, + }); + + return { + id: updated.id, + email: updated.email, + firstName: updated.firstName, + lastName: updated.lastName, + role: updated.role, + isActive: updated.isActive, + twoFactorEnabled: updated.twoFactorEnabled, + }; + } + + /** + * Eigenes Passwort aendern (mit Verifikation des aktuellen Passworts). + */ + async changePassword( + userId: string, + currentPassword: string, + newPassword: string, + ): Promise { + const user = await this.prisma.user.findUnique({ + where: { id: userId }, + include: { authProvider: true }, + }); + + if (!user) { + throw new NotFoundException('Benutzer nicht gefunden'); + } + + const localAuth = user.authProvider.find((ap) => ap.provider === 'LOCAL'); + if (!localAuth?.passwordHash) { + throw new NotFoundException('Kein lokaler Auth-Provider gefunden'); + } + + // Aktuelles Passwort verifizieren + const isCurrentValid = await bcrypt.compare( + currentPassword, + localAuth.passwordHash, + ); + if (!isCurrentValid) { + throw new UnauthorizedException('Aktuelles Passwort ist falsch'); + } + + // Neues Passwort hashen (Bcrypt Cost 12) + const bcryptCost = this.config.get('BCRYPT_COST', 12); + const newHash = await bcrypt.hash(newPassword, bcryptCost); + + // Passwort aktualisieren + await this.prisma.authProvider.update({ + where: { id: localAuth.id }, + data: { passwordHash: newHash }, + }); + + this.logger.log(`Passwort geaendert fuer User ${user.email}`); + } + /** * Alle User auflisten (fuer Admin). */ diff --git a/packages/frontend/src/auth/AuthContext.tsx b/packages/frontend/src/auth/AuthContext.tsx index de423c7..8b0fde2 100644 --- a/packages/frontend/src/auth/AuthContext.tsx +++ b/packages/frontend/src/auth/AuthContext.tsx @@ -14,6 +14,7 @@ interface User { firstName: string; lastName: string; role: string; + twoFactorEnabled: boolean; } interface AuthContextType { @@ -22,6 +23,7 @@ interface AuthContextType { isLoading: boolean; login: (email: string, password: string, totpCode?: string) => Promise; logout: () => Promise; + refreshUser: () => Promise; } interface LoginResult { @@ -95,6 +97,15 @@ export function AuthProvider({ children }: { children: ReactNode }) { [], ); + const refreshUser = useCallback(async () => { + try { + const { data } = await api.get('/users/me'); + setUser(data); + } catch { + // Fehler ignorieren - User bleibt unveraendert + } + }, []); + const logout = useCallback(async () => { try { await api.post('/auth/logout'); @@ -114,6 +125,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { isLoading, login, logout, + refreshUser, }} > {children} diff --git a/packages/frontend/src/profile/ProfilePage.module.css b/packages/frontend/src/profile/ProfilePage.module.css new file mode 100644 index 0000000..f861869 --- /dev/null +++ b/packages/frontend/src/profile/ProfilePage.module.css @@ -0,0 +1,206 @@ +.section { + background: var(--color-bg-card); + border-radius: var(--radius-md); + padding: 1.5rem; + box-shadow: var(--shadow-sm); + border: 1px solid var(--color-border); + margin-bottom: 1.5rem; +} + +.sectionTitle { + font-size: 1.125rem; + font-weight: 600; + margin-bottom: 1.25rem; + padding-bottom: 0.75rem; + border-bottom: 1px solid var(--color-border); +} + +.form { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.field { + display: flex; + flex-direction: column; + gap: 0.375rem; +} + +.field label { + font-size: 0.875rem; + font-weight: 500; + color: var(--color-text); +} + +.field input { + padding: 0.625rem 0.75rem; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + font-size: 0.9375rem; + transition: border-color 0.15s; +} + +.field input:focus { + outline: none; + border-color: var(--color-primary); + box-shadow: 0 0 0 3px var(--color-primary-light); +} + +.field input:disabled { + background: var(--color-bg); + color: var(--color-text-muted); +} + +.field small { + color: var(--color-text-muted); + font-size: 0.75rem; +} + +.fieldRow { + display: flex; + gap: 1rem; +} + +.fieldRow .field { + flex: 1; +} + +.button { + padding: 0.625rem 1.25rem; + background: var(--color-primary); + color: white; + border: none; + border-radius: var(--radius-sm); + font-size: 0.875rem; + font-weight: 600; + cursor: pointer; + transition: background 0.15s; + align-self: flex-start; +} + +.button:hover:not(:disabled) { + background: var(--color-primary-hover); +} + +.button:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +.buttonSecondary { + padding: 0.625rem 1.25rem; + background: transparent; + color: var(--color-text-secondary); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.15s; +} + +.buttonSecondary:hover { + background: var(--color-bg); +} + +.buttonDanger { + padding: 0.625rem 1.25rem; + background: var(--color-error); + color: white; + border: none; + border-radius: var(--radius-sm); + font-size: 0.875rem; + font-weight: 600; + cursor: pointer; + transition: background 0.15s; + align-self: flex-start; +} + +.buttonDanger:hover:not(:disabled) { + background: #b91c1c; +} + +.buttonDanger:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +.buttonRow { + display: flex; + gap: 0.75rem; + align-items: center; +} + +.success { + background: #f0fdf4; + color: var(--color-success); + padding: 0.75rem; + border-radius: var(--radius-sm); + font-size: 0.875rem; + border: 1px solid #bbf7d0; +} + +.error { + background: #fef2f2; + color: var(--color-error); + padding: 0.75rem; + border-radius: var(--radius-sm); + font-size: 0.875rem; + border: 1px solid #fecaca; +} + +.tfaStatus { + display: flex; + align-items: center; + padding: 0.75rem 0; + font-size: 0.9375rem; + margin-bottom: 1rem; +} + +.tfaSetup { + margin-top: 1rem; +} + +.tfaInstructions { + color: var(--color-text-secondary); + font-size: 0.875rem; + margin-bottom: 1rem; +} + +.qrContainer { + display: flex; + justify-content: center; + padding: 1.5rem; + background: white; + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + margin-bottom: 1rem; +} + +.qrContainer img { + width: 200px; + height: 200px; +} + +.manualSecret { + display: flex; + flex-direction: column; + gap: 0.375rem; + margin-bottom: 1.25rem; +} + +.manualSecret label { + font-size: 0.8125rem; + color: var(--color-text-muted); +} + +.manualSecret code { + background: var(--color-bg); + padding: 0.5rem 0.75rem; + border-radius: var(--radius-sm); + font-size: 0.875rem; + letter-spacing: 2px; + word-break: break-all; + border: 1px solid var(--color-border); +} diff --git a/packages/frontend/src/profile/ProfilePage.tsx b/packages/frontend/src/profile/ProfilePage.tsx new file mode 100644 index 0000000..fe9c039 --- /dev/null +++ b/packages/frontend/src/profile/ProfilePage.tsx @@ -0,0 +1,452 @@ +import { useState, type FormEvent } from 'react'; +import { useAuth } from '../auth/AuthContext'; +import api from '../api/client'; +import styles from './ProfilePage.module.css'; + +export function ProfilePage() { + const { user, refreshUser } = useAuth(); + + // --- Persoenliche Informationen --- + const [firstName, setFirstName] = useState(user?.firstName ?? ''); + const [lastName, setLastName] = useState(user?.lastName ?? ''); + const [profileMsg, setProfileMsg] = useState(''); + const [profileError, setProfileError] = useState(''); + const [profileLoading, setProfileLoading] = useState(false); + + // --- Passwort aendern --- + const [currentPassword, setCurrentPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [passwordMsg, setPasswordMsg] = useState(''); + const [passwordError, setPasswordError] = useState(''); + const [passwordLoading, setPasswordLoading] = useState(false); + + // --- 2FA --- + const [twoFactorEnabled, setTwoFactorEnabled] = useState( + user?.twoFactorEnabled ?? false, + ); + const [setupData, setSetupData] = useState<{ + qrCode: string; + secret: string; + } | null>(null); + const [totpCode, setTotpCode] = useState(''); + const [disablePassword, setDisablePassword] = useState(''); + const [showDisableConfirm, setShowDisableConfirm] = useState(false); + const [tfaMsg, setTfaMsg] = useState(''); + const [tfaError, setTfaError] = useState(''); + const [tfaLoading, setTfaLoading] = useState(false); + + // === Handler: Profil aktualisieren === + const handleProfileUpdate = async (e: FormEvent) => { + e.preventDefault(); + setProfileMsg(''); + setProfileError(''); + setProfileLoading(true); + + try { + await api.patch('/users/me', { firstName, lastName }); + await refreshUser(); + setProfileMsg('Profil erfolgreich aktualisiert'); + } catch (err: unknown) { + const error = err as { response?: { data?: { message?: string } } }; + setProfileError( + error.response?.data?.message ?? 'Fehler beim Aktualisieren', + ); + } finally { + setProfileLoading(false); + } + }; + + // === Handler: Passwort aendern === + const handlePasswordChange = async (e: FormEvent) => { + e.preventDefault(); + setPasswordMsg(''); + setPasswordError(''); + + if (newPassword !== confirmPassword) { + setPasswordError('Passwoerter stimmen nicht ueberein'); + return; + } + + if (newPassword.length < 8) { + setPasswordError('Neues Passwort muss mindestens 8 Zeichen lang sein'); + return; + } + + setPasswordLoading(true); + + try { + await api.post('/users/me/change-password', { + currentPassword, + newPassword, + }); + setPasswordMsg('Passwort erfolgreich geaendert'); + setCurrentPassword(''); + setNewPassword(''); + setConfirmPassword(''); + } catch (err: unknown) { + const error = err as { response?: { data?: { message?: string } } }; + setPasswordError( + error.response?.data?.message ?? 'Fehler beim Aendern des Passworts', + ); + } finally { + setPasswordLoading(false); + } + }; + + // === Handler: 2FA Setup starten === + const handleSetup2fa = async () => { + setTfaMsg(''); + setTfaError(''); + setTfaLoading(true); + + try { + const { data } = await api.post<{ qrCode: string; secret: string }>( + '/auth/2fa/setup', + ); + setSetupData(data); + } catch (err: unknown) { + const error = err as { response?: { data?: { message?: string } } }; + setTfaError( + error.response?.data?.message ?? 'Fehler beim 2FA-Setup', + ); + } finally { + setTfaLoading(false); + } + }; + + // === Handler: 2FA aktivieren (Code verifizieren) === + const handleEnable2fa = async (e: FormEvent) => { + e.preventDefault(); + setTfaMsg(''); + setTfaError(''); + setTfaLoading(true); + + try { + await api.post('/auth/2fa/enable', { totpCode }); + setTwoFactorEnabled(true); + setSetupData(null); + setTotpCode(''); + await refreshUser(); + setTfaMsg('2FA wurde erfolgreich aktiviert'); + } catch (err: unknown) { + const error = err as { response?: { data?: { message?: string } } }; + setTfaError( + error.response?.data?.message ?? 'Ungueltiger Code', + ); + } finally { + setTfaLoading(false); + } + }; + + // === Handler: 2FA deaktivieren === + const handleDisable2fa = async (e: FormEvent) => { + e.preventDefault(); + setTfaMsg(''); + setTfaError(''); + setTfaLoading(true); + + try { + await api.post('/auth/2fa/disable', { password: disablePassword }); + setTwoFactorEnabled(false); + setShowDisableConfirm(false); + setDisablePassword(''); + await refreshUser(); + setTfaMsg('2FA wurde erfolgreich deaktiviert'); + } catch (err: unknown) { + const error = err as { response?: { data?: { message?: string } } }; + setTfaError( + error.response?.data?.message ?? 'Fehler beim Deaktivieren', + ); + } finally { + setTfaLoading(false); + } + }; + + return ( +
+

+ Mein Profil +

+ + {/* === Sektion 1: Persoenliche Informationen === */} +
+

Persoenliche Informationen

+
+ {profileMsg &&
{profileMsg}
} + {profileError &&
{profileError}
} + +
+ + + E-Mail-Adresse kann nicht geaendert werden +
+ +
+
+ + setFirstName(e.target.value)} + required + maxLength={100} + /> +
+
+ + setLastName(e.target.value)} + required + maxLength={100} + /> +
+
+ +
+ + +
+ + +
+
+ + {/* === Sektion 2: Passwort aendern === */} +
+

Passwort aendern

+
+ {passwordMsg &&
{passwordMsg}
} + {passwordError && ( +
{passwordError}
+ )} + +
+ + setCurrentPassword(e.target.value)} + required + minLength={8} + /> +
+ +
+ + setNewPassword(e.target.value)} + required + minLength={8} + /> +
+ +
+ + setConfirmPassword(e.target.value)} + required + minLength={8} + /> +
+ + +
+
+ + {/* === Sektion 3: Zwei-Faktor-Authentifizierung === */} +
+

+ Zwei-Faktor-Authentifizierung (2FA) +

+ + {tfaMsg &&
{tfaMsg}
} + {tfaError &&
{tfaError}
} + +
+ + + {twoFactorEnabled + ? '2FA ist aktiviert' + : '2FA ist nicht aktiviert'} + +
+ + {/* 2FA NICHT aktiviert: Setup starten */} + {!twoFactorEnabled && !setupData && ( + + )} + + {/* QR-Code anzeigen + Verifizierung */} + {!twoFactorEnabled && setupData && ( +
+

+ Scannen Sie den QR-Code mit Ihrer Authenticator-App (z.B. Google + Authenticator, Authy): +

+ +
+ QR-Code fuer 2FA +
+ +
+ + {setupData.secret} +
+ +
+
+ + setTotpCode(e.target.value)} + placeholder="6-stelliger Code" + maxLength={6} + pattern="[0-9]{6}" + required + autoFocus + /> + + Geben Sie den 6-stelligen Code aus Ihrer Authenticator-App ein + +
+ +
+ + +
+
+
+ )} + + {/* 2FA aktiviert: Deaktivieren-Option */} + {twoFactorEnabled && !showDisableConfirm && ( + + )} + + {/* Deaktivierung mit Passwort bestaetigen */} + {twoFactorEnabled && showDisableConfirm && ( +
+

+ Geben Sie Ihr Passwort ein, um 2FA zu deaktivieren: +

+
+ + setDisablePassword(e.target.value)} + required + minLength={8} + autoFocus + /> +
+
+ + +
+
+ )} +
+
+ ); +} diff --git a/packages/frontend/src/shell/App.tsx b/packages/frontend/src/shell/App.tsx index 86b7ac9..0b67ea1 100644 --- a/packages/frontend/src/shell/App.tsx +++ b/packages/frontend/src/shell/App.tsx @@ -5,6 +5,7 @@ import { AppLayout } from './AppLayout'; import { DashboardPage } from './DashboardPage'; import { AdminUsersPage } from '../admin/AdminUsersPage'; import { AdminTenantsPage } from '../admin/AdminTenantsPage'; +import { ProfilePage } from '../profile/ProfilePage'; function PrivateRoute({ children }: { children: React.ReactNode }) { const { isAuthenticated, isLoading } = useAuth(); @@ -40,6 +41,7 @@ export function App() { } > } /> + } /> } /> } /> diff --git a/packages/frontend/src/shell/AppLayout.module.css b/packages/frontend/src/shell/AppLayout.module.css index 97bc883..744bc53 100644 --- a/packages/frontend/src/shell/AppLayout.module.css +++ b/packages/frontend/src/shell/AppLayout.module.css @@ -70,6 +70,18 @@ border-top: 1px solid rgba(255, 255, 255, 0.1); } +.userProfile { + cursor: pointer; + padding: 0.375rem; + margin: -0.375rem; + border-radius: var(--radius-sm); + transition: background 0.15s; +} + +.userProfile:hover { + background: rgba(255, 255, 255, 0.1); +} + .userName { font-size: 0.875rem; font-weight: 600; diff --git a/packages/frontend/src/shell/AppLayout.tsx b/packages/frontend/src/shell/AppLayout.tsx index 4b5ce29..0963feb 100644 --- a/packages/frontend/src/shell/AppLayout.tsx +++ b/packages/frontend/src/shell/AppLayout.tsx @@ -55,10 +55,20 @@ export function AppLayout() {
-
- {user?.firstName} {user?.lastName} +
navigate('/profile')} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') navigate('/profile'); + }} + > +
+ {user?.firstName} {user?.lastName} +
+
{user?.email}
-
{user?.email}
From 1643db0e7b3be128c6843e0e647ca49a11419b2e Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Sun, 8 Mar 2026 20:49:41 +0100 Subject: [PATCH 16/50] fix: set refresh token (not access token) in HttpOnly cookie The login endpoint was incorrectly storing the access token in the refresh_token cookie. This caused silent refresh to fail after page reload since the short-lived access token couldn't be used for refresh. Co-Authored-By: Claude Opus 4.6 --- packages/core-service/src/core/auth/auth.controller.ts | 2 +- packages/core-service/src/core/auth/auth.service.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core-service/src/core/auth/auth.controller.ts b/packages/core-service/src/core/auth/auth.controller.ts index 019bd4f..4566aa9 100644 --- a/packages/core-service/src/core/auth/auth.controller.ts +++ b/packages/core-service/src/core/auth/auth.controller.ts @@ -45,7 +45,7 @@ export class AuthController { // Refresh-Token als HttpOnly Cookie setzen (NICHT im localStorage!) // Regel: Kein localStorage fuer Tokens - this.setRefreshTokenCookie(res, result.accessToken); + this.setRefreshTokenCookie(res, result.refreshToken); return { accessToken: result.accessToken, diff --git a/packages/core-service/src/core/auth/auth.service.ts b/packages/core-service/src/core/auth/auth.service.ts index b0cdc51..f12c20f 100644 --- a/packages/core-service/src/core/auth/auth.service.ts +++ b/packages/core-service/src/core/auth/auth.service.ts @@ -21,6 +21,7 @@ interface TokenPair { interface LoginResponse { accessToken: string; + refreshToken: string; user: { id: string; email: string; @@ -151,6 +152,7 @@ export class AuthService { return { accessToken: tokens.accessToken, + refreshToken: tokens.refreshToken, user: { id: user.id, email: user.email, From ffb618ee6539ea9ffb0f46d95ed8cf180a38faed Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Sun, 8 Mar 2026 21:13:31 +0100 Subject: [PATCH 17/50] fix: make refreshToken optional in LoginResponse for 2FA flow The 2FA challenge response does not include a refreshToken (token is only issued after successful 2FA verification). Making the field optional fixes the TS2741 compilation error that prevented the core service from starting. Co-Authored-By: Claude Opus 4.6 --- packages/core-service/src/core/auth/auth.controller.ts | 2 +- packages/core-service/src/core/auth/auth.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core-service/src/core/auth/auth.controller.ts b/packages/core-service/src/core/auth/auth.controller.ts index 4566aa9..697669b 100644 --- a/packages/core-service/src/core/auth/auth.controller.ts +++ b/packages/core-service/src/core/auth/auth.controller.ts @@ -45,7 +45,7 @@ export class AuthController { // Refresh-Token als HttpOnly Cookie setzen (NICHT im localStorage!) // Regel: Kein localStorage fuer Tokens - this.setRefreshTokenCookie(res, result.refreshToken); + this.setRefreshTokenCookie(res, result.refreshToken!); return { accessToken: result.accessToken, diff --git a/packages/core-service/src/core/auth/auth.service.ts b/packages/core-service/src/core/auth/auth.service.ts index f12c20f..cc5f937 100644 --- a/packages/core-service/src/core/auth/auth.service.ts +++ b/packages/core-service/src/core/auth/auth.service.ts @@ -21,7 +21,7 @@ interface TokenPair { interface LoginResponse { accessToken: string; - refreshToken: string; + refreshToken?: string; user: { id: string; email: string; From 6fa86714db939ffe11cd8c3f144545463ff60fe7 Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Mon, 9 Mar 2026 06:51:27 +0100 Subject: [PATCH 18/50] feat: add profile picture upload, sidebar hint, and fix 2FA bugs - Bug fix: include twoFactorEnabled in login response so ProfilePage shows correct 2FA status after login (not always "Aktivieren") - Bug fix: restructure 2FA enable/disable handlers to separate API call from state updates, preventing false error messages on success - New: avatar field in User model (Base64 data-URL in PostgreSQL TEXT) - New: UserAvatar component with initials fallback - New: client-side image resize to 200x200px before upload - New: avatar upload/remove on ProfilePage with preview - New: avatar display + "Zum Profil" hint in sidebar - Increase JSON body size limit to 1mb for avatar uploads Co-Authored-By: Claude Opus 4.6 --- .../core-service/prisma/core.schema.prisma | 1 + .../migration.sql | 2 + .../src/core/auth/auth.service.ts | 2 + .../src/core/users/dto/update-user.dto.ts | 24 ++- .../src/core/users/users.service.ts | 5 + packages/core-service/src/main.ts | 3 + packages/frontend/src/auth/AuthContext.tsx | 1 + .../src/components/UserAvatar.module.css | 15 ++ .../frontend/src/components/UserAvatar.tsx | 40 ++++ .../src/profile/ProfilePage.module.css | 51 +++++ packages/frontend/src/profile/ProfilePage.tsx | 174 ++++++++++++++++-- .../frontend/src/shell/AppLayout.module.css | 27 +++ packages/frontend/src/shell/AppLayout.tsx | 18 +- packages/frontend/src/utils/imageUtils.ts | 51 +++++ 14 files changed, 397 insertions(+), 17 deletions(-) create mode 100644 packages/core-service/prisma/migrations/20260308200000_add_user_avatar/migration.sql create mode 100644 packages/frontend/src/components/UserAvatar.module.css create mode 100644 packages/frontend/src/components/UserAvatar.tsx create mode 100644 packages/frontend/src/utils/imageUtils.ts diff --git a/packages/core-service/prisma/core.schema.prisma b/packages/core-service/prisma/core.schema.prisma index 94ff324..6e67512 100644 --- a/packages/core-service/prisma/core.schema.prisma +++ b/packages/core-service/prisma/core.schema.prisma @@ -23,6 +23,7 @@ model User { email String @unique @db.VarChar(255) firstName String @map("first_name") @db.VarChar(100) lastName String @map("last_name") @db.VarChar(100) + avatar String? @db.Text // Profilbild als Base64 Data-URL role String @default("USER") @db.VarChar(50) // PLATFORM_ADMIN, TENANT_ADMIN, USER isActive Boolean @default(true) @map("is_active") diff --git a/packages/core-service/prisma/migrations/20260308200000_add_user_avatar/migration.sql b/packages/core-service/prisma/migrations/20260308200000_add_user_avatar/migration.sql new file mode 100644 index 0000000..2d75ad1 --- /dev/null +++ b/packages/core-service/prisma/migrations/20260308200000_add_user_avatar/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "users" ADD COLUMN "avatar" TEXT; diff --git a/packages/core-service/src/core/auth/auth.service.ts b/packages/core-service/src/core/auth/auth.service.ts index cc5f937..8d0ccc5 100644 --- a/packages/core-service/src/core/auth/auth.service.ts +++ b/packages/core-service/src/core/auth/auth.service.ts @@ -113,6 +113,7 @@ export class AuthService { firstName: user.firstName, lastName: user.lastName, role: user.role, + twoFactorEnabled: user.twoFactorEnabled, }, requiresTwoFactor: true, }; @@ -159,6 +160,7 @@ export class AuthService { firstName: user.firstName, lastName: user.lastName, role: user.role, + twoFactorEnabled: user.twoFactorEnabled, }, }; } diff --git a/packages/core-service/src/core/users/dto/update-user.dto.ts b/packages/core-service/src/core/users/dto/update-user.dto.ts index d73f6b6..0c2ff34 100644 --- a/packages/core-service/src/core/users/dto/update-user.dto.ts +++ b/packages/core-service/src/core/users/dto/update-user.dto.ts @@ -1,4 +1,11 @@ -import { IsBoolean, IsOptional, IsString, MaxLength } from 'class-validator'; +import { + IsBoolean, + IsOptional, + IsString, + MaxLength, + Matches, + ValidateIf, +} from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; export class UpdateUserDto { @@ -18,4 +25,19 @@ export class UpdateUserDto { @IsOptional() @IsBoolean() isActive?: boolean; + + @ApiProperty({ + description: 'Profilbild als Base64 Data-URL (max. 400KB), null zum Entfernen', + example: 'data:image/png;base64,iVBORw0KGgo...', + required: false, + nullable: true, + }) + @IsOptional() + @ValidateIf((o: UpdateUserDto) => o.avatar !== null) + @IsString() + @MaxLength(400000, { message: 'Profilbild darf maximal 400KB gross sein' }) + @Matches(/^data:image\/(jpeg|png|gif|webp);base64,[A-Za-z0-9+/=]+$/, { + message: 'Profilbild muss ein gueltiges Base64-Bild sein (JPEG, PNG, GIF oder WebP)', + }) + avatar?: string | null; } diff --git a/packages/core-service/src/core/users/users.service.ts b/packages/core-service/src/core/users/users.service.ts index 9b00363..18d785a 100644 --- a/packages/core-service/src/core/users/users.service.ts +++ b/packages/core-service/src/core/users/users.service.ts @@ -95,6 +95,7 @@ export class UsersService { email: user.email, firstName: user.firstName, lastName: user.lastName, + avatar: user.avatar, role: user.role, isActive: user.isActive, twoFactorEnabled: user.twoFactorEnabled, @@ -124,6 +125,7 @@ export class UsersService { firstName: dto.firstName, lastName: dto.lastName, isActive: dto.isActive, + ...(dto.avatar !== undefined && { avatar: dto.avatar }), }, }); @@ -132,6 +134,7 @@ export class UsersService { email: updated.email, firstName: updated.firstName, lastName: updated.lastName, + avatar: updated.avatar, role: updated.role, isActive: updated.isActive, }; @@ -151,6 +154,7 @@ export class UsersService { data: { ...(dto.firstName !== undefined && { firstName: dto.firstName }), ...(dto.lastName !== undefined && { lastName: dto.lastName }), + ...(dto.avatar !== undefined && { avatar: dto.avatar }), }, }); @@ -159,6 +163,7 @@ export class UsersService { email: updated.email, firstName: updated.firstName, lastName: updated.lastName, + avatar: updated.avatar, role: updated.role, isActive: updated.isActive, twoFactorEnabled: updated.twoFactorEnabled, diff --git a/packages/core-service/src/main.ts b/packages/core-service/src/main.ts index ae0050f..64173cd 100644 --- a/packages/core-service/src/main.ts +++ b/packages/core-service/src/main.ts @@ -15,6 +15,9 @@ async function bootstrap(): Promise { app.use(helmet()); app.use(cookieParser()); + // Body size limit erhoehen fuer Base64 Avatar-Uploads (Standard ~100KB) + app.useBodyParser('json', { limit: '1mb' }); + // CORS const corsOrigins = process.env.CORS_ORIGINS?.split(',') ?? [ 'http://172.20.10.59', diff --git a/packages/frontend/src/auth/AuthContext.tsx b/packages/frontend/src/auth/AuthContext.tsx index 8b0fde2..96fc15b 100644 --- a/packages/frontend/src/auth/AuthContext.tsx +++ b/packages/frontend/src/auth/AuthContext.tsx @@ -13,6 +13,7 @@ interface User { email: string; firstName: string; lastName: string; + avatar?: string | null; role: string; twoFactorEnabled: boolean; } diff --git a/packages/frontend/src/components/UserAvatar.module.css b/packages/frontend/src/components/UserAvatar.module.css new file mode 100644 index 0000000..4b067b3 --- /dev/null +++ b/packages/frontend/src/components/UserAvatar.module.css @@ -0,0 +1,15 @@ +.avatar { + border-radius: 50%; + object-fit: cover; + flex-shrink: 0; +} + +.initials { + display: flex; + align-items: center; + justify-content: center; + background: #3b82f6; + color: white; + font-weight: 600; + user-select: none; +} diff --git a/packages/frontend/src/components/UserAvatar.tsx b/packages/frontend/src/components/UserAvatar.tsx new file mode 100644 index 0000000..7dff6ae --- /dev/null +++ b/packages/frontend/src/components/UserAvatar.tsx @@ -0,0 +1,40 @@ +import styles from './UserAvatar.module.css'; + +interface UserAvatarProps { + firstName: string; + lastName: string; + avatar?: string | null; + size?: number; + className?: string; +} + +export function UserAvatar({ + firstName, + lastName, + avatar, + size = 36, + className, +}: UserAvatarProps) { + const initials = `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase(); + + if (avatar) { + return ( + {`${firstName} + ); + } + + return ( +
+ {initials} +
+ ); +} diff --git a/packages/frontend/src/profile/ProfilePage.module.css b/packages/frontend/src/profile/ProfilePage.module.css index f861869..31c1727 100644 --- a/packages/frontend/src/profile/ProfilePage.module.css +++ b/packages/frontend/src/profile/ProfilePage.module.css @@ -204,3 +204,54 @@ word-break: break-all; border: 1px solid var(--color-border); } + +/* === Avatar === */ +.avatarSection { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.75rem; + padding-bottom: 1.25rem; + margin-bottom: 1.25rem; + border-bottom: 1px solid var(--color-border); +} + +.avatarPreview { + position: relative; + cursor: pointer; + border-radius: 50%; + overflow: hidden; +} + +.avatarOverlay { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.5); + color: white; + font-size: 0.75rem; + font-weight: 500; + opacity: 0; + transition: opacity 0.2s; + border-radius: 50%; +} + +.avatarPreview:hover .avatarOverlay { + opacity: 1; +} + +.avatarActions { + display: flex; + gap: 0.5rem; +} + +.avatarHint { + color: var(--color-text-muted); + font-size: 0.75rem; +} + +.hiddenInput { + display: none; +} diff --git a/packages/frontend/src/profile/ProfilePage.tsx b/packages/frontend/src/profile/ProfilePage.tsx index fe9c039..8207a47 100644 --- a/packages/frontend/src/profile/ProfilePage.tsx +++ b/packages/frontend/src/profile/ProfilePage.tsx @@ -1,6 +1,8 @@ -import { useState, type FormEvent } from 'react'; +import { useState, useEffect, useRef, type FormEvent, type ChangeEvent } from 'react'; import { useAuth } from '../auth/AuthContext'; import api from '../api/client'; +import { UserAvatar } from '../components/UserAvatar'; +import { resizeImageToBase64 } from '../utils/imageUtils'; import styles from './ProfilePage.module.css'; export function ProfilePage() { @@ -13,6 +15,13 @@ export function ProfilePage() { const [profileError, setProfileError] = useState(''); const [profileLoading, setProfileLoading] = useState(false); + // --- Profilbild --- + const [avatar, setAvatar] = useState(user?.avatar ?? null); + const [avatarMsg, setAvatarMsg] = useState(''); + const [avatarError, setAvatarError] = useState(''); + const [avatarLoading, setAvatarLoading] = useState(false); + const fileInputRef = useRef(null); + // --- Passwort aendern --- const [currentPassword, setCurrentPassword] = useState(''); const [newPassword, setNewPassword] = useState(''); @@ -36,6 +45,79 @@ export function ProfilePage() { const [tfaError, setTfaError] = useState(''); const [tfaLoading, setTfaLoading] = useState(false); + // 2FA-Status mit Context-User synchronisieren (Bug-Fix: Login-Response hat jetzt twoFactorEnabled) + useEffect(() => { + if (user?.twoFactorEnabled !== undefined) { + setTwoFactorEnabled(user.twoFactorEnabled); + } + }, [user?.twoFactorEnabled]); + + // Avatar mit Context-User synchronisieren + useEffect(() => { + if (user?.avatar !== undefined) { + setAvatar(user.avatar ?? null); + } + }, [user?.avatar]); + + // === Handler: Profilbild hochladen === + const handleAvatarChange = async (e: ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + if (!file.type.startsWith('image/')) { + setAvatarError('Bitte waehlen Sie eine Bilddatei aus'); + return; + } + + if (file.size > 5 * 1024 * 1024) { + setAvatarError('Bild darf maximal 5MB gross sein'); + return; + } + + setAvatarMsg(''); + setAvatarError(''); + setAvatarLoading(true); + + try { + const base64 = await resizeImageToBase64(file, 200, 200); + await api.patch('/users/me', { avatar: base64 }); + setAvatar(base64); + await refreshUser(); + setAvatarMsg('Profilbild erfolgreich aktualisiert'); + } catch (err: unknown) { + const error = err as { response?: { data?: { message?: string } } }; + setAvatarError( + error.response?.data?.message ?? 'Fehler beim Hochladen des Profilbilds', + ); + } finally { + setAvatarLoading(false); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + } + }; + + // === Handler: Profilbild entfernen === + const handleAvatarRemove = async () => { + setAvatarMsg(''); + setAvatarError(''); + setAvatarLoading(true); + + try { + await api.patch('/users/me', { avatar: null }); + setAvatar(null); + await refreshUser(); + setAvatarMsg('Profilbild entfernt'); + } catch (err: unknown) { + const error = err as { response?: { data?: { message?: string } } }; + setAvatarError( + error.response?.data?.message ?? 'Fehler beim Entfernen des Profilbilds', + ); + } finally { + setAvatarLoading(false); + } + }; + // === Handler: Profil aktualisieren === const handleProfileUpdate = async (e: FormEvent) => { e.preventDefault(); @@ -124,19 +206,24 @@ export function ProfilePage() { try { await api.post('/auth/2fa/enable', { totpCode }); - setTwoFactorEnabled(true); - setSetupData(null); - setTotpCode(''); - await refreshUser(); - setTfaMsg('2FA wurde erfolgreich aktiviert'); } catch (err: unknown) { const error = err as { response?: { data?: { message?: string } } }; setTfaError( error.response?.data?.message ?? 'Ungueltiger Code', ); - } finally { setTfaLoading(false); + return; } + + // Erfolg — State aktualisieren + setTwoFactorEnabled(true); + setSetupData(null); + setTotpCode(''); + setTfaMsg('2FA wurde erfolgreich aktiviert'); + setTfaLoading(false); + + // User-Context im Hintergrund aktualisieren + refreshUser().catch(() => {}); }; // === Handler: 2FA deaktivieren === @@ -148,19 +235,24 @@ export function ProfilePage() { try { await api.post('/auth/2fa/disable', { password: disablePassword }); - setTwoFactorEnabled(false); - setShowDisableConfirm(false); - setDisablePassword(''); - await refreshUser(); - setTfaMsg('2FA wurde erfolgreich deaktiviert'); } catch (err: unknown) { const error = err as { response?: { data?: { message?: string } } }; setTfaError( error.response?.data?.message ?? 'Fehler beim Deaktivieren', ); - } finally { setTfaLoading(false); + return; } + + // Erfolg — State aktualisieren + setTwoFactorEnabled(false); + setShowDisableConfirm(false); + setDisablePassword(''); + setTfaMsg('2FA wurde erfolgreich deaktiviert'); + setTfaLoading(false); + + // User-Context im Hintergrund aktualisieren + refreshUser().catch(() => {}); }; return ( @@ -178,6 +270,62 @@ export function ProfilePage() { {/* === Sektion 1: Persoenliche Informationen === */}

Persoenliche Informationen

+ + {/* Profilbild */} +
+
fileInputRef.current?.click()} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') fileInputRef.current?.click(); + }} + > + +
+ Bild aendern +
+
+ +
+ + {avatar && ( + + )} +
+ {avatarMsg &&
{avatarMsg}
} + {avatarError &&
{avatarError}
} + + JPEG, PNG, GIF oder WebP. Wird auf 200x200 Pixel skaliert. + +
+
{profileMsg &&
{profileMsg}
} {profileError &&
{profileError}
} diff --git a/packages/frontend/src/shell/AppLayout.module.css b/packages/frontend/src/shell/AppLayout.module.css index 744bc53..b46860b 100644 --- a/packages/frontend/src/shell/AppLayout.module.css +++ b/packages/frontend/src/shell/AppLayout.module.css @@ -82,15 +82,42 @@ background: rgba(255, 255, 255, 0.1); } +.userProfileInner { + display: flex; + align-items: center; + gap: 0.625rem; +} + +.userDetails { + min-width: 0; +} + .userName { font-size: 0.875rem; font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .userEmail { font-size: 0.75rem; color: rgba(255, 255, 255, 0.5); margin-top: 0.125rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.profileHint { + font-size: 0.6875rem; + color: rgba(255, 255, 255, 0.35); + margin-top: 0.375rem; + transition: color 0.15s; +} + +.userProfile:hover .profileHint { + color: rgba(255, 255, 255, 0.7); } .logoutBtn { diff --git a/packages/frontend/src/shell/AppLayout.tsx b/packages/frontend/src/shell/AppLayout.tsx index 0963feb..810a548 100644 --- a/packages/frontend/src/shell/AppLayout.tsx +++ b/packages/frontend/src/shell/AppLayout.tsx @@ -1,5 +1,6 @@ import { Outlet, NavLink, useNavigate } from 'react-router-dom'; import { useAuth } from '../auth/AuthContext'; +import { UserAvatar } from '../components/UserAvatar'; import styles from './AppLayout.module.css'; export function AppLayout() { @@ -64,10 +65,21 @@ export function AppLayout() { if (e.key === 'Enter' || e.key === ' ') navigate('/profile'); }} > -
- {user?.firstName} {user?.lastName} +
+ +
+
+ {user?.firstName} {user?.lastName} +
+
{user?.email}
+
-
{user?.email}
+
Zum Profil →
+ + +
- {/* Profilbild */} -
-
fileInputRef.current?.click()} - role="button" - tabIndex={0} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') fileInputRef.current?.click(); - }} - > - -
- Bild aendern -
-
- -
- - {avatar && ( + +
+ Bild ändern +
+
+ +
- )} + {avatar && ( + + )} +
+ {avatarMsg &&
{avatarMsg}
} + {avatarError &&
{avatarError}
} + + JPEG, PNG, GIF oder WebP. Wird auf 200x200 Pixel skaliert. +
- {avatarMsg &&
{avatarMsg}
} - {avatarError &&
{avatarError}
} - - JPEG, PNG, GIF oder WebP. Wird auf 200x200 Pixel skaliert. - + + + {profileMsg &&
{profileMsg}
} + {profileError &&
{profileError}
} + +
+ + + E-Mail-Adresse kann nicht geändert werden +
+ +
+
+ + setFirstName(e.target.value)} + required + maxLength={100} + /> +
+
+ + setLastName(e.target.value)} + required + maxLength={100} + /> +
+
+ +
+ + +
+ + +
+ )} -
- {profileMsg &&
{profileMsg}
} - {profileError &&
{profileError}
} + {/* === Tab: Experten Profil (Platzhalter) === */} + {activeTab === 'expert' && ( +
+

Experten Profil

+

+ Hier können Sie zukünftig Ihr Experten-Profil verwalten. +

+
+ )} -
- - - E-Mail-Adresse kann nicht geaendert werden -
+ {/* === Tab: Passwort ändern === */} + {activeTab === 'password' && ( +
+

Passwort ändern

+ + {passwordMsg &&
{passwordMsg}
} + {passwordError && ( +
{passwordError}
+ )} -
- + setFirstName(e.target.value)} + id="currentPassword" + type="password" + value={currentPassword} + onChange={(e) => setCurrentPassword(e.target.value)} required - maxLength={100} + minLength={8} />
+
- + setLastName(e.target.value)} + id="newPassword" + type="password" + value={newPassword} + onChange={(e) => setNewPassword(e.target.value)} required - maxLength={100} + minLength={8} />
-
-
- - -
+
+ + setConfirmPassword(e.target.value)} + required + minLength={8} + /> +
- - -
+ + +
+ )} - {/* === Sektion 2: Passwort aendern === */} -
-

Passwort aendern

-
- {passwordMsg &&
{passwordMsg}
} - {passwordError && ( -
{passwordError}
- )} - -
- - setCurrentPassword(e.target.value)} - required - minLength={8} - /> -
- -
- - setNewPassword(e.target.value)} - required - minLength={8} - /> -
- -
- - setConfirmPassword(e.target.value)} - required - minLength={8} - /> -
- - -
-
- - {/* === Sektion 3: Zwei-Faktor-Authentifizierung === */} + {/* === Sicherheit: Zwei-Faktor-Authentifizierung (immer sichtbar) === */}

Zwei-Faktor-Authentifizierung (2FA) @@ -486,17 +528,17 @@ export function ProfilePage() {

- QR-Code fuer 2FA + QR-Code für 2FA
- + {setupData.secret}
- + )} - {/* Deaktivierung mit Passwort bestaetigen */} + {/* Deaktivierung mit Passwort bestätigen */} {twoFactorEnabled && showDisableConfirm && (

- {/* Oeffentliche Routen */} + {/* Öffentliche Routen */} } /> - {/* Geschuetzte Routen */} + {/* Geschützte Routen */} Date: Mon, 9 Mar 2026 09:03:15 +0100 Subject: [PATCH 21/50] feat: restructure profile page with new layout, contact fields, and 2FA relocation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add phone, mobile, street, postalCode, city fields to User model (Prisma + migration) - Extend UpdateUserDto with validated contact/address fields - Update UsersService (findById, update, updateProfile) for new fields - Rename tab "Persönliche Informationen" to "Profil" - New layout: avatar left column, form right column with fieldset groups - Move 2FA section from always-visible into "Passwort ändern" tab - Add orange 2FA warning badge next to page title (clickable → password tab) - Add responsive CSS for mobile breakpoint Co-Authored-By: Claude Opus 4.6 --- .../core-service/prisma/core.schema.prisma | 10 + .../migration.sql | 6 + .../src/core/users/dto/update-user.dto.ts | 37 + .../src/core/users/users.service.ts | 25 + packages/frontend/src/auth/AuthContext.tsx | 5 + .../src/profile/ProfilePage.module.css | 130 +++- packages/frontend/src/profile/ProfilePage.tsx | 723 ++++++++++-------- 7 files changed, 623 insertions(+), 313 deletions(-) create mode 100644 packages/core-service/prisma/migrations/20260309000000_add_user_contact_fields/migration.sql diff --git a/packages/core-service/prisma/core.schema.prisma b/packages/core-service/prisma/core.schema.prisma index 6e67512..99a03bd 100644 --- a/packages/core-service/prisma/core.schema.prisma +++ b/packages/core-service/prisma/core.schema.prisma @@ -24,6 +24,16 @@ model User { firstName String @map("first_name") @db.VarChar(100) lastName String @map("last_name") @db.VarChar(100) avatar String? @db.Text // Profilbild als Base64 Data-URL + + // Kontaktdaten + phone String? @map("phone") @db.VarChar(30) + mobile String? @map("mobile") @db.VarChar(30) + + // Adresse + street String? @map("street") @db.VarChar(200) + postalCode String? @map("postal_code") @db.VarChar(10) + city String? @map("city") @db.VarChar(100) + role String @default("USER") @db.VarChar(50) // PLATFORM_ADMIN, TENANT_ADMIN, USER isActive Boolean @default(true) @map("is_active") diff --git a/packages/core-service/prisma/migrations/20260309000000_add_user_contact_fields/migration.sql b/packages/core-service/prisma/migrations/20260309000000_add_user_contact_fields/migration.sql new file mode 100644 index 0000000..94d27a5 --- /dev/null +++ b/packages/core-service/prisma/migrations/20260309000000_add_user_contact_fields/migration.sql @@ -0,0 +1,6 @@ +-- AlterTable: Kontakt- und Adressfelder für Benutzerprofil +ALTER TABLE "users" ADD COLUMN "phone" VARCHAR(30); +ALTER TABLE "users" ADD COLUMN "mobile" VARCHAR(30); +ALTER TABLE "users" ADD COLUMN "street" VARCHAR(200); +ALTER TABLE "users" ADD COLUMN "postal_code" VARCHAR(10); +ALTER TABLE "users" ADD COLUMN "city" VARCHAR(100); diff --git a/packages/core-service/src/core/users/dto/update-user.dto.ts b/packages/core-service/src/core/users/dto/update-user.dto.ts index 111397a..998818b 100644 --- a/packages/core-service/src/core/users/dto/update-user.dto.ts +++ b/packages/core-service/src/core/users/dto/update-user.dto.ts @@ -40,4 +40,41 @@ export class UpdateUserDto { message: 'Profilbild muss ein gültiges Base64-Bild sein (JPEG, PNG, GIF oder WebP)', }) avatar?: string | null; + + // --- Kontaktdaten --- + @ApiProperty({ example: '+49 123 456789', required: false, nullable: true }) + @IsOptional() + @ValidateIf((o: UpdateUserDto) => o.phone !== null) + @IsString() + @MaxLength(30) + phone?: string | null; + + @ApiProperty({ example: '+49 170 1234567', required: false, nullable: true }) + @IsOptional() + @ValidateIf((o: UpdateUserDto) => o.mobile !== null) + @IsString() + @MaxLength(30) + mobile?: string | null; + + // --- Adresse --- + @ApiProperty({ example: 'Musterstraße 42', required: false, nullable: true }) + @IsOptional() + @ValidateIf((o: UpdateUserDto) => o.street !== null) + @IsString() + @MaxLength(200) + street?: string | null; + + @ApiProperty({ example: '12345', required: false, nullable: true }) + @IsOptional() + @ValidateIf((o: UpdateUserDto) => o.postalCode !== null) + @IsString() + @MaxLength(10) + postalCode?: string | null; + + @ApiProperty({ example: 'Berlin', required: false, nullable: true }) + @IsOptional() + @ValidateIf((o: UpdateUserDto) => o.city !== null) + @IsString() + @MaxLength(100) + city?: string | null; } diff --git a/packages/core-service/src/core/users/users.service.ts b/packages/core-service/src/core/users/users.service.ts index e9f38b2..f678807 100644 --- a/packages/core-service/src/core/users/users.service.ts +++ b/packages/core-service/src/core/users/users.service.ts @@ -96,6 +96,11 @@ export class UsersService { firstName: user.firstName, lastName: user.lastName, avatar: user.avatar, + phone: user.phone, + mobile: user.mobile, + street: user.street, + postalCode: user.postalCode, + city: user.city, role: user.role, isActive: user.isActive, twoFactorEnabled: user.twoFactorEnabled, @@ -126,6 +131,11 @@ export class UsersService { lastName: dto.lastName, isActive: dto.isActive, ...(dto.avatar !== undefined && { avatar: dto.avatar }), + ...(dto.phone !== undefined && { phone: dto.phone }), + ...(dto.mobile !== undefined && { mobile: dto.mobile }), + ...(dto.street !== undefined && { street: dto.street }), + ...(dto.postalCode !== undefined && { postalCode: dto.postalCode }), + ...(dto.city !== undefined && { city: dto.city }), }, }); @@ -135,6 +145,11 @@ export class UsersService { firstName: updated.firstName, lastName: updated.lastName, avatar: updated.avatar, + phone: updated.phone, + mobile: updated.mobile, + street: updated.street, + postalCode: updated.postalCode, + city: updated.city, role: updated.role, isActive: updated.isActive, }; @@ -155,6 +170,11 @@ export class UsersService { ...(dto.firstName !== undefined && { firstName: dto.firstName }), ...(dto.lastName !== undefined && { lastName: dto.lastName }), ...(dto.avatar !== undefined && { avatar: dto.avatar }), + ...(dto.phone !== undefined && { phone: dto.phone }), + ...(dto.mobile !== undefined && { mobile: dto.mobile }), + ...(dto.street !== undefined && { street: dto.street }), + ...(dto.postalCode !== undefined && { postalCode: dto.postalCode }), + ...(dto.city !== undefined && { city: dto.city }), }, }); @@ -164,6 +184,11 @@ export class UsersService { firstName: updated.firstName, lastName: updated.lastName, avatar: updated.avatar, + phone: updated.phone, + mobile: updated.mobile, + street: updated.street, + postalCode: updated.postalCode, + city: updated.city, role: updated.role, isActive: updated.isActive, twoFactorEnabled: updated.twoFactorEnabled, diff --git a/packages/frontend/src/auth/AuthContext.tsx b/packages/frontend/src/auth/AuthContext.tsx index ce5a802..8f88831 100644 --- a/packages/frontend/src/auth/AuthContext.tsx +++ b/packages/frontend/src/auth/AuthContext.tsx @@ -14,6 +14,11 @@ interface User { firstName: string; lastName: string; avatar?: string | null; + phone?: string | null; + mobile?: string | null; + street?: string | null; + postalCode?: string | null; + city?: string | null; role: string; twoFactorEnabled: boolean; } diff --git a/packages/frontend/src/profile/ProfilePage.module.css b/packages/frontend/src/profile/ProfilePage.module.css index a9cedc2..be3ae44 100644 --- a/packages/frontend/src/profile/ProfilePage.module.css +++ b/packages/frontend/src/profile/ProfilePage.module.css @@ -54,6 +54,29 @@ border-bottom: 1px solid var(--color-border); } +/* === Profil-Layout (Avatar links, Formular rechts) === */ +.profileLayout { + display: flex; + gap: 2rem; + align-items: flex-start; +} + +.avatarColumn { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.75rem; + min-width: 140px; + flex-shrink: 0; + padding-top: 0.25rem; +} + +.formColumn { + flex: 1; + min-width: 0; +} + +/* === Formular === */ .form { display: flex; flex-direction: column; @@ -64,6 +87,7 @@ display: flex; flex-direction: column; gap: 0.375rem; + flex: 1; } .field label { @@ -105,6 +129,31 @@ flex: 1; } +.fieldSmall { + flex: 0 0 120px !important; +} + +/* === Fieldset-Gruppen === */ +.fieldGroup { + border: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.fieldGroupLegend { + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--color-text-muted); + padding: 0; + margin-bottom: 0.125rem; +} + +/* === Buttons === */ .button { padding: 0.625rem 1.25rem; background: var(--color-primary); @@ -128,12 +177,12 @@ } .buttonSecondary { - padding: 0.625rem 1.25rem; + padding: 0.5rem 1rem; background: transparent; color: var(--color-text-secondary); border: 1px solid var(--color-border); border-radius: var(--radius-sm); - font-size: 0.875rem; + font-size: 0.8125rem; font-weight: 500; cursor: pointer; transition: all 0.15s; @@ -144,12 +193,12 @@ } .buttonDanger { - padding: 0.625rem 1.25rem; + padding: 0.5rem 1rem; background: var(--color-error); color: white; border: none; border-radius: var(--radius-sm); - font-size: 0.875rem; + font-size: 0.8125rem; font-weight: 600; cursor: pointer; transition: background 0.15s; @@ -171,6 +220,7 @@ align-items: center; } +/* === Meldungen === */ .success { background: #f0fdf4; color: var(--color-success); @@ -189,6 +239,7 @@ border: 1px solid #fecaca; } +/* === 2FA === */ .tfaStatus { display: flex; align-items: center; @@ -244,17 +295,34 @@ border: 1px solid var(--color-border); } -/* === Avatar === */ -.avatarSection { - display: flex; - flex-direction: column; +/* === 2FA Warn-Badge === */ +.tfaWarning { + display: inline-flex; align-items: center; - gap: 0.75rem; - padding-bottom: 1.25rem; - margin-bottom: 1.25rem; - border-bottom: 1px solid var(--color-border); + gap: 0.375rem; + padding: 0.25rem 0.75rem; + background: #fff7ed; + color: #c2410c; + border: 1px solid #fed7aa; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 600; + cursor: pointer; + transition: all 0.15s; + white-space: nowrap; } +.tfaWarning::before { + content: '\26A0'; + font-size: 0.875rem; +} + +.tfaWarning:hover { + background: #ffedd5; + border-color: #fdba74; +} + +/* === Avatar === */ .avatarPreview { position: relative; cursor: pointer; @@ -288,9 +356,45 @@ .avatarHint { color: var(--color-text-muted); - font-size: 0.75rem; + font-size: 0.6875rem; + text-align: center; } .hiddenInput { display: none; } + +/* === Responsive === */ +@media (max-width: 640px) { + .profileLayout { + flex-direction: column; + align-items: center; + } + + .avatarColumn { + min-width: unset; + padding-bottom: 1.25rem; + border-bottom: 1px solid var(--color-border); + width: 100%; + } + + .fieldRow { + flex-direction: column; + gap: 1rem; + } + + .fieldSmall { + flex: 1 !important; + } + + .tabs { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + + .tab { + padding: 0.625rem 1rem; + font-size: 0.875rem; + white-space: nowrap; + } +} diff --git a/packages/frontend/src/profile/ProfilePage.tsx b/packages/frontend/src/profile/ProfilePage.tsx index 7b94f84..5c6d54c 100644 --- a/packages/frontend/src/profile/ProfilePage.tsx +++ b/packages/frontend/src/profile/ProfilePage.tsx @@ -13,6 +13,11 @@ export function ProfilePage() { // --- Persönliche Informationen --- const [firstName, setFirstName] = useState(user?.firstName ?? ''); const [lastName, setLastName] = useState(user?.lastName ?? ''); + const [phone, setPhone] = useState(user?.phone ?? ''); + const [mobile, setMobile] = useState(user?.mobile ?? ''); + const [street, setStreet] = useState(user?.street ?? ''); + const [postalCode, setPostalCode] = useState(user?.postalCode ?? ''); + const [city, setCity] = useState(user?.city ?? ''); const [profileMsg, setProfileMsg] = useState(''); const [profileError, setProfileError] = useState(''); const [profileLoading, setProfileLoading] = useState(false); @@ -47,7 +52,7 @@ export function ProfilePage() { const [tfaError, setTfaError] = useState(''); const [tfaLoading, setTfaLoading] = useState(false); - // 2FA-Status mit Context-User synchronisieren (Bug-Fix: Login-Response hat jetzt twoFactorEnabled) + // 2FA-Status mit Context-User synchronisieren useEffect(() => { if (user?.twoFactorEnabled !== undefined) { setTwoFactorEnabled(user.twoFactorEnabled); @@ -61,6 +66,17 @@ export function ProfilePage() { } }, [user?.avatar]); + // Kontaktdaten mit Context-User synchronisieren + useEffect(() => { + if (user) { + setPhone(user.phone ?? ''); + setMobile(user.mobile ?? ''); + setStreet(user.street ?? ''); + setPostalCode(user.postalCode ?? ''); + setCity(user.city ?? ''); + } + }, [user?.phone, user?.mobile, user?.street, user?.postalCode, user?.city]); + // === Handler: Profilbild hochladen === const handleAvatarChange = async (e: ChangeEvent) => { const file = e.target.files?.[0]; @@ -128,7 +144,15 @@ export function ProfilePage() { setProfileLoading(true); try { - await api.patch('/users/me', { firstName, lastName }); + await api.patch('/users/me', { + firstName, + lastName, + phone: phone || null, + mobile: mobile || null, + street: street || null, + postalCode: postalCode || null, + city: city || null, + }); await refreshUser(); setProfileMsg('Profil erfolgreich aktualisiert'); } catch (err: unknown) { @@ -267,9 +291,26 @@ export function ProfilePage() { fontSize: '1.5rem', fontWeight: 600, marginBottom: '1.5rem', + display: 'flex', + alignItems: 'center', + gap: '0.75rem', }} > Mein Profil + {!twoFactorEnabled && ( + setActiveTab('password')} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') setActiveTab('password'); + }} + title="Klicken, um 2FA zu aktivieren" + > + 2FA nicht aktiv + + )}

{/* === Tab-Leiste === */} @@ -279,7 +320,7 @@ export function ProfilePage() { className={`${styles.tab} ${activeTab === 'personal' ? styles.tabActive : ''}`} onClick={() => setActiveTab('personal')} > - Persönliche Informationen + Profil
- {/* === Tab: Persönliche Informationen === */} + {/* === Tab: Profil === */} {activeTab === 'personal' && (
- {/* Profilbild */} -
-
fileInputRef.current?.click()} - role="button" - tabIndex={0} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') fileInputRef.current?.click(); - }} - > - -
- Bild ändern -
-
- -
- - {avatar && ( + +
+ Ändern +
+
+ +
- )} + {avatar && ( + + )} +
+ {avatarMsg &&
{avatarMsg}
} + {avatarError &&
{avatarError}
} + + JPEG, PNG, GIF oder WebP. Max. 200x200px. + +
+ + {/* --- Rechte Spalte: Formular --- */} +
+ + {profileMsg &&
{profileMsg}
} + {profileError &&
{profileError}
} + + {/* Name */} +
+ Name +
+
+ + setFirstName(e.target.value)} + required + maxLength={100} + /> +
+
+ + setLastName(e.target.value)} + required + maxLength={100} + /> +
+
+
+ + {/* E-Mail & Rolle */} +
+
+ + + E-Mail kann nicht geändert werden +
+
+ + +
+
+ + {/* Telefon */} +
+ Kontakt +
+
+ + setPhone(e.target.value)} + maxLength={30} + placeholder="+49 123 456789" + /> +
+
+ + setMobile(e.target.value)} + maxLength={30} + placeholder="+49 170 1234567" + /> +
+
+
+ + {/* Adresse */} +
+ Adresse +
+ + setStreet(e.target.value)} + maxLength={200} + placeholder="Musterstraße 42" + /> +
+
+
+ + setPostalCode(e.target.value)} + maxLength={10} + placeholder="12345" + /> +
+
+ + setCity(e.target.value)} + maxLength={100} + placeholder="Berlin" + /> +
+
+
+ + +
- {avatarMsg &&
{avatarMsg}
} - {avatarError &&
{avatarError}
} - - JPEG, PNG, GIF oder WebP. Wird auf 200x200 Pixel skaliert. -
- -
- {profileMsg &&
{profileMsg}
} - {profileError &&
{profileError}
} - -
- - - E-Mail-Adresse kann nicht geändert werden -
- -
-
- - setFirstName(e.target.value)} - required - maxLength={100} - /> -
-
- - setLastName(e.target.value)} - required - maxLength={100} - /> -
-
- -
- - -
- - -
)} @@ -421,222 +542,224 @@ export function ProfilePage() { )} - {/* === Tab: Passwort ändern === */} + {/* === Tab: Passwort ändern + 2FA === */} {activeTab === 'password' && ( -
-

Passwort ändern

-
- {passwordMsg &&
{passwordMsg}
} - {passwordError && ( -
{passwordError}
- )} + <> +
+

Passwort ändern

+ + {passwordMsg &&
{passwordMsg}
} + {passwordError && ( +
{passwordError}
+ )} -
- - setCurrentPassword(e.target.value)} - required - minLength={8} - /> -
- -
- - setNewPassword(e.target.value)} - required - minLength={8} - /> -
- -
- - setConfirmPassword(e.target.value)} - required - minLength={8} - /> -
- - - -
- )} - - {/* === Sicherheit: Zwei-Faktor-Authentifizierung (immer sichtbar) === */} -
-

- Zwei-Faktor-Authentifizierung (2FA) -

- - {tfaMsg &&
{tfaMsg}
} - {tfaError &&
{tfaError}
} - -
- - - {twoFactorEnabled - ? '2FA ist aktiviert' - : '2FA ist nicht aktiviert'} - -
- - {/* 2FA NICHT aktiviert: Setup starten */} - {!twoFactorEnabled && !setupData && ( - - )} - - {/* QR-Code anzeigen + Verifizierung */} - {!twoFactorEnabled && setupData && ( -
-

- Scannen Sie den QR-Code mit Ihrer Authenticator-App (z.B. Google - Authenticator, Authy): -

- -
- QR-Code für 2FA -
- -
- - {setupData.secret} -
- -
- + setTotpCode(e.target.value)} - placeholder="6-stelliger Code" - maxLength={6} - pattern="[0-9]{6}" + id="currentPassword" + type="password" + value={currentPassword} + onChange={(e) => setCurrentPassword(e.target.value)} required - autoFocus + minLength={8} /> - - Geben Sie den 6-stelligen Code aus Ihrer Authenticator-App ein -
-
- - +
+ + setNewPassword(e.target.value)} + required + minLength={8} + />
- -
- )} - {/* 2FA aktiviert: Deaktivieren-Option */} - {twoFactorEnabled && !showDisableConfirm && ( - - )} +
+ + setConfirmPassword(e.target.value)} + required + minLength={8} + /> +
- {/* Deaktivierung mit Passwort bestätigen */} - {twoFactorEnabled && showDisableConfirm && ( -
-

- Geben Sie Ihr Passwort ein, um 2FA zu deaktivieren: -

-
- - setDisablePassword(e.target.value)} - required - minLength={8} - autoFocus - /> -
-
+ +
+ + {/* === 2FA Sektion (innerhalb Passwort-Tab) === */} +
+

+ Zwei-Faktor-Authentifizierung (2FA) +

+ + {tfaMsg &&
{tfaMsg}
} + {tfaError &&
{tfaError}
} + +
+ + + {twoFactorEnabled + ? '2FA ist aktiviert' + : '2FA ist nicht aktiviert'} + +
+ + {/* 2FA NICHT aktiviert: Setup starten */} + {!twoFactorEnabled && !setupData && ( -
- - )} -
+ )} + + {/* QR-Code anzeigen + Verifizierung */} + {!twoFactorEnabled && setupData && ( +
+

+ Scannen Sie den QR-Code mit Ihrer Authenticator-App (z.B. Google + Authenticator, Authy): +

+ +
+ QR-Code für 2FA +
+ +
+ + {setupData.secret} +
+ +
+
+ + setTotpCode(e.target.value)} + placeholder="6-stelliger Code" + maxLength={6} + pattern="[0-9]{6}" + required + autoFocus + /> + + Geben Sie den 6-stelligen Code aus Ihrer Authenticator-App ein + +
+ +
+ + +
+
+
+ )} + + {/* 2FA aktiviert: Deaktivieren-Option */} + {twoFactorEnabled && !showDisableConfirm && ( + + )} + + {/* Deaktivierung mit Passwort bestätigen */} + {twoFactorEnabled && showDisableConfirm && ( +
+

+ Geben Sie Ihr Passwort ein, um 2FA zu deaktivieren: +

+
+ + setDisablePassword(e.target.value)} + required + minLength={8} + autoFocus + /> +
+
+ + +
+
+ )} +
+ + )}
); } From b326081c543011d78541a2db2b58ff59428136a6 Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Mon, 9 Mar 2026 10:23:47 +0100 Subject: [PATCH 22/50] feat: implement expert profile with skills, experience, languages, projects, certifications and attachments Full-stack implementation of the Expert Profile tab with 6 sections: - Skills (tag/chip UI with add/remove) - Experience (area, years, optional level) - Languages (language + proficiency level) - Project History (modal form with dates, role, tasks, company details) - Certifications (modal form with title, issuer, website, year) - Attachments (file upload/download as Base64, max 10MB) Backend: 15 API endpoints, 8 DTOs, full CRUD service with ownership verification. Frontend: Reusable Modal component (React Portal), ExpertProfileTab orchestrator, 8 section components. Database: 6 new tables (expert_profiles, expert_experiences, expert_languages, expert_projects, expert_certifications, expert_attachments). Co-Authored-By: Claude Opus 4.6 --- .../core-service/prisma/core.schema.prisma | 135 +++++ .../migration.sql | 106 ++++ packages/core-service/src/app.module.ts | 2 + .../dto/create-certification.dto.ts | 29 ++ .../dto/create-experience.dto.ts | 25 + .../expert-profile/dto/create-language.dto.ts | 17 + .../expert-profile/dto/create-project.dto.ts | 82 +++ .../dto/update-certification.dto.ts | 30 ++ .../expert-profile/dto/update-project.dto.ts | 79 +++ .../expert-profile/dto/update-skills.dto.ts | 14 + .../dto/upload-attachment.dto.ts | 28 ++ .../expert-profile.controller.ts | 190 +++++++ .../expert-profile/expert-profile.module.ts | 10 + .../expert-profile/expert-profile.service.ts | 319 ++++++++++++ packages/core-service/src/main.ts | 4 +- .../frontend/src/components/Modal.module.css | 86 ++++ packages/frontend/src/components/Modal.tsx | 60 +++ .../src/profile/ExpertProfileTab.module.css | 469 ++++++++++++++++++ .../frontend/src/profile/ExpertProfileTab.tsx | 111 +++++ packages/frontend/src/profile/ProfilePage.tsx | 12 +- .../profile/sections/AttachmentsSection.tsx | 165 ++++++ .../profile/sections/CertificationModal.tsx | 134 +++++ .../sections/CertificationsSection.tsx | 106 ++++ .../profile/sections/ExperienceSection.tsx | 129 +++++ .../src/profile/sections/LanguagesSection.tsx | 114 +++++ .../src/profile/sections/ProjectModal.tsx | 243 +++++++++ .../src/profile/sections/ProjectsSection.tsx | 117 +++++ .../src/profile/sections/SkillsSection.tsx | 108 ++++ 28 files changed, 2913 insertions(+), 11 deletions(-) create mode 100644 packages/core-service/prisma/migrations/20260309100000_add_expert_profile/migration.sql create mode 100644 packages/core-service/src/core/expert-profile/dto/create-certification.dto.ts create mode 100644 packages/core-service/src/core/expert-profile/dto/create-experience.dto.ts create mode 100644 packages/core-service/src/core/expert-profile/dto/create-language.dto.ts create mode 100644 packages/core-service/src/core/expert-profile/dto/create-project.dto.ts create mode 100644 packages/core-service/src/core/expert-profile/dto/update-certification.dto.ts create mode 100644 packages/core-service/src/core/expert-profile/dto/update-project.dto.ts create mode 100644 packages/core-service/src/core/expert-profile/dto/update-skills.dto.ts create mode 100644 packages/core-service/src/core/expert-profile/dto/upload-attachment.dto.ts create mode 100644 packages/core-service/src/core/expert-profile/expert-profile.controller.ts create mode 100644 packages/core-service/src/core/expert-profile/expert-profile.module.ts create mode 100644 packages/core-service/src/core/expert-profile/expert-profile.service.ts create mode 100644 packages/frontend/src/components/Modal.module.css create mode 100644 packages/frontend/src/components/Modal.tsx create mode 100644 packages/frontend/src/profile/ExpertProfileTab.module.css create mode 100644 packages/frontend/src/profile/ExpertProfileTab.tsx create mode 100644 packages/frontend/src/profile/sections/AttachmentsSection.tsx create mode 100644 packages/frontend/src/profile/sections/CertificationModal.tsx create mode 100644 packages/frontend/src/profile/sections/CertificationsSection.tsx create mode 100644 packages/frontend/src/profile/sections/ExperienceSection.tsx create mode 100644 packages/frontend/src/profile/sections/LanguagesSection.tsx create mode 100644 packages/frontend/src/profile/sections/ProjectModal.tsx create mode 100644 packages/frontend/src/profile/sections/ProjectsSection.tsx create mode 100644 packages/frontend/src/profile/sections/SkillsSection.tsx diff --git a/packages/core-service/prisma/core.schema.prisma b/packages/core-service/prisma/core.schema.prisma index 99a03bd..8e31890 100644 --- a/packages/core-service/prisma/core.schema.prisma +++ b/packages/core-service/prisma/core.schema.prisma @@ -53,6 +53,7 @@ model User { authProvider AuthProvider[] tenantMemberships TenantMembership[] auditLogs AuditLog[] + expertProfile ExpertProfile? @@map("users") } @@ -190,3 +191,137 @@ model AuditLog { @@index([createdAt]) @@map("audit_logs") } + +// -------------------------------------------------------- +// ExpertProfile - Experten-Profil (1:1 mit User) +// -------------------------------------------------------- +model ExpertProfile { + id String @id @default(uuid()) @db.Uuid + userId String @unique @map("user_id") @db.Uuid + + // Skills als Tag-Array + skills String[] @default([]) + + // Timestamps + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + // Relationen + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + experiences ExpertExperience[] + languages ExpertLanguage[] + projects ExpertProject[] + certifications ExpertCertification[] + attachments ExpertAttachment[] + + @@map("expert_profiles") +} + +// -------------------------------------------------------- +// ExpertExperience - Erfahrung / Expertise-Bereiche +// -------------------------------------------------------- +model ExpertExperience { + id String @id @default(uuid()) @db.Uuid + expertProfileId String @map("expert_profile_id") @db.Uuid + area String @db.VarChar(200) // z.B. "IT Infrastruktur" + years Int // Jahre Erfahrung + level String? @db.VarChar(50) // Experte, Fortgeschritten, Grundkenntnisse + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + // Relationen + expertProfile ExpertProfile @relation(fields: [expertProfileId], references: [id], onDelete: Cascade) + + @@map("expert_experiences") +} + +// -------------------------------------------------------- +// ExpertLanguage - Sprachen +// -------------------------------------------------------- +model ExpertLanguage { + id String @id @default(uuid()) @db.Uuid + expertProfileId String @map("expert_profile_id") @db.Uuid + language String @db.VarChar(100) // z.B. "Deutsch" + level String @db.VarChar(20) // Muttersprache, C2, C1, B2, B1, A2, A1 + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + // Relationen + expertProfile ExpertProfile @relation(fields: [expertProfileId], references: [id], onDelete: Cascade) + + @@map("expert_languages") +} + +// -------------------------------------------------------- +// ExpertProject - Projekthistorie +// -------------------------------------------------------- +model ExpertProject { + id String @id @default(uuid()) @db.Uuid + expertProfileId String @map("expert_profile_id") @db.Uuid + + // Zeitraum + fromMonth Int @map("from_month") // 1-12 + fromYear Int @map("from_year") // z.B. 2023 + toMonth Int? @map("to_month") // null wenn isCurrent + toYear Int? @map("to_year") + isCurrent Boolean @default(false) @map("is_current") + + // Details + role String @db.VarChar(200) // Taetigkeit + tasks String? @db.Text // Aufgaben (max 1500 Zeichen im DTO) + company String? @db.VarChar(200) // Firma + companySize String? @map("company_size") @db.VarChar(20) // "1-10", "11-50", etc. + industry String? @db.VarChar(200) // Branche + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + // Relationen + expertProfile ExpertProfile @relation(fields: [expertProfileId], references: [id], onDelete: Cascade) + + @@index([expertProfileId, fromYear, fromMonth]) + @@map("expert_projects") +} + +// -------------------------------------------------------- +// ExpertCertification - Zertifizierungen +// -------------------------------------------------------- +model ExpertCertification { + id String @id @default(uuid()) @db.Uuid + expertProfileId String @map("expert_profile_id") @db.Uuid + + title String @db.VarChar(300) // Titel + issuingBody String @map("issuing_body") @db.VarChar(300) // Zertifizierungsstelle + website String? @db.VarChar(500) // URL + issueYear Int @map("issue_year") // Ausstellungsjahr + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + // Relationen + expertProfile ExpertProfile @relation(fields: [expertProfileId], references: [id], onDelete: Cascade) + + @@map("expert_certifications") +} + +// -------------------------------------------------------- +// ExpertAttachment - Profilanlagen (Dateien als Base64) +// -------------------------------------------------------- +model ExpertAttachment { + id String @id @default(uuid()) @db.Uuid + expertProfileId String @map("expert_profile_id") @db.Uuid + + filename String @db.VarChar(255) + mimetype String @db.VarChar(100) + size Int // Groesse in Bytes + data String @db.Text // Base64-Daten + + createdAt DateTime @default(now()) @map("created_at") + + // Relationen + expertProfile ExpertProfile @relation(fields: [expertProfileId], references: [id], onDelete: Cascade) + + @@map("expert_attachments") +} diff --git a/packages/core-service/prisma/migrations/20260309100000_add_expert_profile/migration.sql b/packages/core-service/prisma/migrations/20260309100000_add_expert_profile/migration.sql new file mode 100644 index 0000000..4bbac07 --- /dev/null +++ b/packages/core-service/prisma/migrations/20260309100000_add_expert_profile/migration.sql @@ -0,0 +1,106 @@ +-- CreateTable: expert_profiles +CREATE TABLE "expert_profiles" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "user_id" UUID NOT NULL, + "skills" TEXT[] DEFAULT ARRAY[]::TEXT[], + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "expert_profiles_pkey" PRIMARY KEY ("id") +); + +-- CreateTable: expert_experiences +CREATE TABLE "expert_experiences" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "expert_profile_id" UUID NOT NULL, + "area" VARCHAR(200) NOT NULL, + "years" INTEGER NOT NULL, + "level" VARCHAR(50), + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "expert_experiences_pkey" PRIMARY KEY ("id") +); + +-- CreateTable: expert_languages +CREATE TABLE "expert_languages" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "expert_profile_id" UUID NOT NULL, + "language" VARCHAR(100) NOT NULL, + "level" VARCHAR(20) NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "expert_languages_pkey" PRIMARY KEY ("id") +); + +-- CreateTable: expert_projects +CREATE TABLE "expert_projects" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "expert_profile_id" UUID NOT NULL, + "from_month" INTEGER NOT NULL, + "from_year" INTEGER NOT NULL, + "to_month" INTEGER, + "to_year" INTEGER, + "is_current" BOOLEAN NOT NULL DEFAULT false, + "role" VARCHAR(200) NOT NULL, + "tasks" TEXT, + "company" VARCHAR(200), + "company_size" VARCHAR(20), + "industry" VARCHAR(200), + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "expert_projects_pkey" PRIMARY KEY ("id") +); + +-- CreateTable: expert_certifications +CREATE TABLE "expert_certifications" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "expert_profile_id" UUID NOT NULL, + "title" VARCHAR(300) NOT NULL, + "issuing_body" VARCHAR(300) NOT NULL, + "website" VARCHAR(500), + "issue_year" INTEGER NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "expert_certifications_pkey" PRIMARY KEY ("id") +); + +-- CreateTable: expert_attachments +CREATE TABLE "expert_attachments" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "expert_profile_id" UUID NOT NULL, + "filename" VARCHAR(255) NOT NULL, + "mimetype" VARCHAR(100) NOT NULL, + "size" INTEGER NOT NULL, + "data" TEXT NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "expert_attachments_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "expert_profiles_user_id_key" ON "expert_profiles"("user_id"); + +-- CreateIndex +CREATE INDEX "expert_projects_expert_profile_id_from_year_from_month_idx" ON "expert_projects"("expert_profile_id", "from_year", "from_month"); + +-- AddForeignKey +ALTER TABLE "expert_profiles" ADD CONSTRAINT "expert_profiles_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "expert_experiences" ADD CONSTRAINT "expert_experiences_expert_profile_id_fkey" FOREIGN KEY ("expert_profile_id") REFERENCES "expert_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "expert_languages" ADD CONSTRAINT "expert_languages_expert_profile_id_fkey" FOREIGN KEY ("expert_profile_id") REFERENCES "expert_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "expert_projects" ADD CONSTRAINT "expert_projects_expert_profile_id_fkey" FOREIGN KEY ("expert_profile_id") REFERENCES "expert_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "expert_certifications" ADD CONSTRAINT "expert_certifications_expert_profile_id_fkey" FOREIGN KEY ("expert_profile_id") REFERENCES "expert_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "expert_attachments" ADD CONSTRAINT "expert_attachments_expert_profile_id_fkey" FOREIGN KEY ("expert_profile_id") REFERENCES "expert_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/core-service/src/app.module.ts b/packages/core-service/src/app.module.ts index c86c293..09d531b 100644 --- a/packages/core-service/src/app.module.ts +++ b/packages/core-service/src/app.module.ts @@ -9,6 +9,7 @@ import { RedisModule } from './redis/redis.module'; import { AuthModule } from './core/auth/auth.module'; import { UsersModule } from './core/users/users.module'; import { TenantsModule } from './core/tenants/tenants.module'; +import { ExpertProfileModule } from './core/expert-profile/expert-profile.module'; import { JwtAuthGuard } from './common/guards/jwt-auth.guard'; import { validateConfig } from './config/env.validation'; @@ -40,6 +41,7 @@ import { validateConfig } from './config/env.validation'; AuthModule, UsersModule, TenantsModule, + ExpertProfileModule, ], providers: [ // Global Guards: Alle Routen sind standardmaessig geschuetzt diff --git a/packages/core-service/src/core/expert-profile/dto/create-certification.dto.ts b/packages/core-service/src/core/expert-profile/dto/create-certification.dto.ts new file mode 100644 index 0000000..de3593f --- /dev/null +++ b/packages/core-service/src/core/expert-profile/dto/create-certification.dto.ts @@ -0,0 +1,29 @@ +import { IsString, IsInt, IsOptional, IsNotEmpty, MaxLength, Min, Max, IsUrl } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateCertificationDto { + @ApiProperty({ example: 'AWS Solutions Architect Professional' }) + @IsString() + @IsNotEmpty({ message: 'Titel ist erforderlich' }) + @MaxLength(300) + title!: string; + + @ApiProperty({ example: 'Amazon Web Services' }) + @IsString() + @IsNotEmpty({ message: 'Zertifizierungsstelle ist erforderlich' }) + @MaxLength(300) + issuingBody!: string; + + @ApiProperty({ example: 'https://aws.amazon.com/certification/', required: false }) + @IsOptional() + @IsString() + @MaxLength(500) + @IsUrl({}, { message: 'Bitte eine gültige URL angeben' }) + website?: string; + + @ApiProperty({ example: 2024, description: 'Ausstellungsjahr' }) + @IsInt() + @Min(1970) + @Max(2100) + issueYear!: number; +} diff --git a/packages/core-service/src/core/expert-profile/dto/create-experience.dto.ts b/packages/core-service/src/core/expert-profile/dto/create-experience.dto.ts new file mode 100644 index 0000000..23c5df1 --- /dev/null +++ b/packages/core-service/src/core/expert-profile/dto/create-experience.dto.ts @@ -0,0 +1,25 @@ +import { IsString, IsInt, IsOptional, MaxLength, Min, Max, IsIn } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateExperienceDto { + @ApiProperty({ example: 'IT Infrastruktur' }) + @IsString() + @MaxLength(200) + area!: string; + + @ApiProperty({ example: 10 }) + @IsInt() + @Min(0, { message: 'Jahre dürfen nicht negativ sein' }) + @Max(60, { message: 'Maximal 60 Jahre Erfahrung' }) + years!: number; + + @ApiProperty({ + example: 'Experte', + required: false, + enum: ['Experte', 'Fortgeschritten', 'Grundkenntnisse'], + }) + @IsOptional() + @IsString() + @IsIn(['Experte', 'Fortgeschritten', 'Grundkenntnisse']) + level?: string; +} diff --git a/packages/core-service/src/core/expert-profile/dto/create-language.dto.ts b/packages/core-service/src/core/expert-profile/dto/create-language.dto.ts new file mode 100644 index 0000000..200a54e --- /dev/null +++ b/packages/core-service/src/core/expert-profile/dto/create-language.dto.ts @@ -0,0 +1,17 @@ +import { IsString, MaxLength, IsIn } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateLanguageDto { + @ApiProperty({ example: 'Deutsch' }) + @IsString() + @MaxLength(100) + language!: string; + + @ApiProperty({ + example: 'Muttersprache', + enum: ['Muttersprache', 'C2', 'C1', 'B2', 'B1', 'A2', 'A1'], + }) + @IsString() + @IsIn(['Muttersprache', 'C2', 'C1', 'B2', 'B1', 'A2', 'A1']) + level!: string; +} diff --git a/packages/core-service/src/core/expert-profile/dto/create-project.dto.ts b/packages/core-service/src/core/expert-profile/dto/create-project.dto.ts new file mode 100644 index 0000000..ae9e152 --- /dev/null +++ b/packages/core-service/src/core/expert-profile/dto/create-project.dto.ts @@ -0,0 +1,82 @@ +import { + IsString, + IsInt, + IsOptional, + IsBoolean, + IsNotEmpty, + MaxLength, + Min, + Max, + IsIn, + ValidateIf, +} from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateProjectDto { + @ApiProperty({ example: 3, description: 'Startmonat (1-12)' }) + @IsInt() + @Min(1) + @Max(12) + fromMonth!: number; + + @ApiProperty({ example: 2023, description: 'Startjahr' }) + @IsInt() + @Min(1970) + @Max(2100) + fromYear!: number; + + @ApiProperty({ example: 6, required: false, description: 'Endmonat (1-12)' }) + @IsOptional() + @ValidateIf((o: CreateProjectDto) => !o.isCurrent) + @IsInt() + @Min(1) + @Max(12) + toMonth?: number; + + @ApiProperty({ example: 2024, required: false, description: 'Endjahr' }) + @IsOptional() + @ValidateIf((o: CreateProjectDto) => !o.isCurrent) + @IsInt() + @Min(1970) + @Max(2100) + toYear?: number; + + @ApiProperty({ example: false, required: false, description: 'Projekt läuft noch' }) + @IsOptional() + @IsBoolean() + isCurrent?: boolean; + + @ApiProperty({ example: 'Senior DevOps Engineer' }) + @IsString() + @IsNotEmpty({ message: 'Tätigkeit ist erforderlich' }) + @MaxLength(200) + role!: string; + + @ApiProperty({ example: 'Aufbau und Betrieb der Kubernetes-Infrastruktur', required: false }) + @IsOptional() + @IsString() + @MaxLength(1500, { message: 'Aufgaben dürfen maximal 1500 Zeichen lang sein' }) + tasks?: string; + + @ApiProperty({ example: 'Xinion GmbH', required: false }) + @IsOptional() + @IsString() + @MaxLength(200) + company?: string; + + @ApiProperty({ + example: '51-200', + required: false, + enum: ['1-10', '11-50', '51-200', '201-500', '501-1000', '1001-5000', '5000+'], + }) + @IsOptional() + @IsString() + @IsIn(['1-10', '11-50', '51-200', '201-500', '501-1000', '1001-5000', '5000+']) + companySize?: string; + + @ApiProperty({ example: 'IT-Dienstleistung', required: false }) + @IsOptional() + @IsString() + @MaxLength(200) + industry?: string; +} diff --git a/packages/core-service/src/core/expert-profile/dto/update-certification.dto.ts b/packages/core-service/src/core/expert-profile/dto/update-certification.dto.ts new file mode 100644 index 0000000..863792d --- /dev/null +++ b/packages/core-service/src/core/expert-profile/dto/update-certification.dto.ts @@ -0,0 +1,30 @@ +import { IsString, IsInt, IsOptional, MaxLength, Min, Max, IsUrl } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class UpdateCertificationDto { + @ApiProperty({ example: 'AWS Solutions Architect Professional', required: false }) + @IsOptional() + @IsString() + @MaxLength(300) + title?: string; + + @ApiProperty({ example: 'Amazon Web Services', required: false }) + @IsOptional() + @IsString() + @MaxLength(300) + issuingBody?: string; + + @ApiProperty({ example: 'https://aws.amazon.com/certification/', required: false }) + @IsOptional() + @IsString() + @MaxLength(500) + @IsUrl({}, { message: 'Bitte eine gültige URL angeben' }) + website?: string; + + @ApiProperty({ example: 2024, required: false }) + @IsOptional() + @IsInt() + @Min(1970) + @Max(2100) + issueYear?: number; +} diff --git a/packages/core-service/src/core/expert-profile/dto/update-project.dto.ts b/packages/core-service/src/core/expert-profile/dto/update-project.dto.ts new file mode 100644 index 0000000..4510077 --- /dev/null +++ b/packages/core-service/src/core/expert-profile/dto/update-project.dto.ts @@ -0,0 +1,79 @@ +import { + IsString, + IsInt, + IsOptional, + IsBoolean, + MaxLength, + Min, + Max, + IsIn, + ValidateIf, +} from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class UpdateProjectDto { + @ApiProperty({ example: 3, required: false }) + @IsOptional() + @IsInt() + @Min(1) + @Max(12) + fromMonth?: number; + + @ApiProperty({ example: 2023, required: false }) + @IsOptional() + @IsInt() + @Min(1970) + @Max(2100) + fromYear?: number; + + @ApiProperty({ example: 6, required: false }) + @IsOptional() + @ValidateIf((o: UpdateProjectDto) => !o.isCurrent) + @IsInt() + @Min(1) + @Max(12) + toMonth?: number; + + @ApiProperty({ example: 2024, required: false }) + @IsOptional() + @ValidateIf((o: UpdateProjectDto) => !o.isCurrent) + @IsInt() + @Min(1970) + @Max(2100) + toYear?: number; + + @ApiProperty({ example: false, required: false }) + @IsOptional() + @IsBoolean() + isCurrent?: boolean; + + @ApiProperty({ example: 'Senior DevOps Engineer', required: false }) + @IsOptional() + @IsString() + @MaxLength(200) + role?: string; + + @ApiProperty({ example: 'Aufbau der K8s-Infrastruktur', required: false }) + @IsOptional() + @IsString() + @MaxLength(1500, { message: 'Aufgaben dürfen maximal 1500 Zeichen lang sein' }) + tasks?: string; + + @ApiProperty({ example: 'Xinion GmbH', required: false }) + @IsOptional() + @IsString() + @MaxLength(200) + company?: string; + + @ApiProperty({ example: '51-200', required: false }) + @IsOptional() + @IsString() + @IsIn(['1-10', '11-50', '51-200', '201-500', '501-1000', '1001-5000', '5000+']) + companySize?: string; + + @ApiProperty({ example: 'IT-Dienstleistung', required: false }) + @IsOptional() + @IsString() + @MaxLength(200) + industry?: string; +} diff --git a/packages/core-service/src/core/expert-profile/dto/update-skills.dto.ts b/packages/core-service/src/core/expert-profile/dto/update-skills.dto.ts new file mode 100644 index 0000000..c1802b6 --- /dev/null +++ b/packages/core-service/src/core/expert-profile/dto/update-skills.dto.ts @@ -0,0 +1,14 @@ +import { IsArray, IsString, MaxLength, ArrayMaxSize } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class UpdateSkillsDto { + @ApiProperty({ + example: ['Kubernetes', 'Docker', 'AWS', 'Terraform'], + description: 'Komplettes Skills-Array (ersetzt vorhandene Skills)', + }) + @IsArray() + @IsString({ each: true }) + @MaxLength(100, { each: true, message: 'Jeder Skill darf maximal 100 Zeichen lang sein' }) + @ArrayMaxSize(50, { message: 'Maximal 50 Skills erlaubt' }) + skills!: string[]; +} diff --git a/packages/core-service/src/core/expert-profile/dto/upload-attachment.dto.ts b/packages/core-service/src/core/expert-profile/dto/upload-attachment.dto.ts new file mode 100644 index 0000000..042dc9a --- /dev/null +++ b/packages/core-service/src/core/expert-profile/dto/upload-attachment.dto.ts @@ -0,0 +1,28 @@ +import { IsString, IsInt, IsNotEmpty, MaxLength, Min, Max } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class UploadAttachmentDto { + @ApiProperty({ example: 'AWS-Zertifikat.pdf' }) + @IsString() + @IsNotEmpty({ message: 'Dateiname ist erforderlich' }) + @MaxLength(255) + filename!: string; + + @ApiProperty({ example: 'application/pdf' }) + @IsString() + @IsNotEmpty() + @MaxLength(100) + mimetype!: string; + + @ApiProperty({ example: 524288, description: 'Dateigröße in Bytes' }) + @IsInt() + @Min(1, { message: 'Datei darf nicht leer sein' }) + @Max(10485760, { message: 'Datei darf maximal 10MB groß sein' }) + size!: number; + + @ApiProperty({ description: 'Base64-kodierte Dateidaten' }) + @IsString() + @IsNotEmpty() + @MaxLength(14000000, { message: 'Base64-Daten dürfen maximal ~10MB betragen' }) + data!: string; +} diff --git a/packages/core-service/src/core/expert-profile/expert-profile.controller.ts b/packages/core-service/src/core/expert-profile/expert-profile.controller.ts new file mode 100644 index 0000000..9469fa8 --- /dev/null +++ b/packages/core-service/src/core/expert-profile/expert-profile.controller.ts @@ -0,0 +1,190 @@ +import { + Controller, + Get, + Patch, + Post, + Delete, + Param, + Body, + ParseUUIDPipe, + HttpCode, + HttpStatus, +} from '@nestjs/common'; +import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; +import { CurrentUser } from '../../common/decorators/current-user.decorator'; +import { ExpertProfileService } from './expert-profile.service'; +import { UpdateSkillsDto } from './dto/update-skills.dto'; +import { CreateExperienceDto } from './dto/create-experience.dto'; +import { CreateLanguageDto } from './dto/create-language.dto'; +import { CreateProjectDto } from './dto/create-project.dto'; +import { UpdateProjectDto } from './dto/update-project.dto'; +import { CreateCertificationDto } from './dto/create-certification.dto'; +import { UpdateCertificationDto } from './dto/update-certification.dto'; +import { UploadAttachmentDto } from './dto/upload-attachment.dto'; + +@ApiTags('Experten-Profil') +@ApiBearerAuth('access-token') +@Controller('expert-profile') +export class ExpertProfileController { + constructor(private readonly expertProfileService: ExpertProfileService) {} + + // ============================================================ + // Profil + // ============================================================ + @Get('me') + @ApiOperation({ summary: 'Eigenes Experten-Profil abrufen' }) + async getProfile(@CurrentUser('sub') userId: string) { + return this.expertProfileService.getOrCreateProfile(userId); + } + + // ============================================================ + // Skills + // ============================================================ + @Patch('me/skills') + @ApiOperation({ summary: 'Skills aktualisieren' }) + async updateSkills( + @CurrentUser('sub') userId: string, + @Body() dto: UpdateSkillsDto, + ) { + return this.expertProfileService.updateSkills(userId, dto); + } + + // ============================================================ + // Erfahrung + // ============================================================ + @Post('me/experiences') + @ApiOperation({ summary: 'Erfahrung hinzufügen' }) + async addExperience( + @CurrentUser('sub') userId: string, + @Body() dto: CreateExperienceDto, + ) { + return this.expertProfileService.addExperience(userId, dto); + } + + @Delete('me/experiences/:id') + @HttpCode(HttpStatus.NO_CONTENT) + @ApiOperation({ summary: 'Erfahrung löschen' }) + async deleteExperience( + @CurrentUser('sub') userId: string, + @Param('id', ParseUUIDPipe) id: string, + ) { + await this.expertProfileService.deleteExperience(userId, id); + } + + // ============================================================ + // Sprachen + // ============================================================ + @Post('me/languages') + @ApiOperation({ summary: 'Sprache hinzufügen' }) + async addLanguage( + @CurrentUser('sub') userId: string, + @Body() dto: CreateLanguageDto, + ) { + return this.expertProfileService.addLanguage(userId, dto); + } + + @Delete('me/languages/:id') + @HttpCode(HttpStatus.NO_CONTENT) + @ApiOperation({ summary: 'Sprache löschen' }) + async deleteLanguage( + @CurrentUser('sub') userId: string, + @Param('id', ParseUUIDPipe) id: string, + ) { + await this.expertProfileService.deleteLanguage(userId, id); + } + + // ============================================================ + // Projekte + // ============================================================ + @Post('me/projects') + @ApiOperation({ summary: 'Projekt hinzufügen' }) + async addProject( + @CurrentUser('sub') userId: string, + @Body() dto: CreateProjectDto, + ) { + return this.expertProfileService.addProject(userId, dto); + } + + @Patch('me/projects/:id') + @ApiOperation({ summary: 'Projekt bearbeiten' }) + async updateProject( + @CurrentUser('sub') userId: string, + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: UpdateProjectDto, + ) { + return this.expertProfileService.updateProject(userId, id, dto); + } + + @Delete('me/projects/:id') + @HttpCode(HttpStatus.NO_CONTENT) + @ApiOperation({ summary: 'Projekt löschen' }) + async deleteProject( + @CurrentUser('sub') userId: string, + @Param('id', ParseUUIDPipe) id: string, + ) { + await this.expertProfileService.deleteProject(userId, id); + } + + // ============================================================ + // Zertifizierungen + // ============================================================ + @Post('me/certifications') + @ApiOperation({ summary: 'Zertifizierung hinzufügen' }) + async addCertification( + @CurrentUser('sub') userId: string, + @Body() dto: CreateCertificationDto, + ) { + return this.expertProfileService.addCertification(userId, dto); + } + + @Patch('me/certifications/:id') + @ApiOperation({ summary: 'Zertifizierung bearbeiten' }) + async updateCertification( + @CurrentUser('sub') userId: string, + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: UpdateCertificationDto, + ) { + return this.expertProfileService.updateCertification(userId, id, dto); + } + + @Delete('me/certifications/:id') + @HttpCode(HttpStatus.NO_CONTENT) + @ApiOperation({ summary: 'Zertifizierung löschen' }) + async deleteCertification( + @CurrentUser('sub') userId: string, + @Param('id', ParseUUIDPipe) id: string, + ) { + await this.expertProfileService.deleteCertification(userId, id); + } + + // ============================================================ + // Profilanlagen + // ============================================================ + @Post('me/attachments') + @ApiOperation({ summary: 'Datei hochladen' }) + async uploadAttachment( + @CurrentUser('sub') userId: string, + @Body() dto: UploadAttachmentDto, + ) { + return this.expertProfileService.uploadAttachment(userId, dto); + } + + @Get('me/attachments/:id') + @ApiOperation({ summary: 'Datei herunterladen' }) + async downloadAttachment( + @CurrentUser('sub') userId: string, + @Param('id', ParseUUIDPipe) id: string, + ) { + return this.expertProfileService.downloadAttachment(userId, id); + } + + @Delete('me/attachments/:id') + @HttpCode(HttpStatus.NO_CONTENT) + @ApiOperation({ summary: 'Datei löschen' }) + async deleteAttachment( + @CurrentUser('sub') userId: string, + @Param('id', ParseUUIDPipe) id: string, + ) { + await this.expertProfileService.deleteAttachment(userId, id); + } +} diff --git a/packages/core-service/src/core/expert-profile/expert-profile.module.ts b/packages/core-service/src/core/expert-profile/expert-profile.module.ts new file mode 100644 index 0000000..7d5ec38 --- /dev/null +++ b/packages/core-service/src/core/expert-profile/expert-profile.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ExpertProfileController } from './expert-profile.controller'; +import { ExpertProfileService } from './expert-profile.service'; + +@Module({ + controllers: [ExpertProfileController], + providers: [ExpertProfileService], + exports: [ExpertProfileService], +}) +export class ExpertProfileModule {} diff --git a/packages/core-service/src/core/expert-profile/expert-profile.service.ts b/packages/core-service/src/core/expert-profile/expert-profile.service.ts new file mode 100644 index 0000000..9b2a9b4 --- /dev/null +++ b/packages/core-service/src/core/expert-profile/expert-profile.service.ts @@ -0,0 +1,319 @@ +import { + Injectable, + NotFoundException, + ForbiddenException, + Logger, +} from '@nestjs/common'; +import { PrismaService } from '../../prisma/prisma.service'; +import { UpdateSkillsDto } from './dto/update-skills.dto'; +import { CreateExperienceDto } from './dto/create-experience.dto'; +import { CreateLanguageDto } from './dto/create-language.dto'; +import { CreateProjectDto } from './dto/create-project.dto'; +import { UpdateProjectDto } from './dto/update-project.dto'; +import { CreateCertificationDto } from './dto/create-certification.dto'; +import { UpdateCertificationDto } from './dto/update-certification.dto'; +import { UploadAttachmentDto } from './dto/upload-attachment.dto'; + +@Injectable() +export class ExpertProfileService { + private readonly logger = new Logger(ExpertProfileService.name); + + constructor(private readonly prisma: PrismaService) {} + + // ============================================================ + // Profil laden / auto-erstellen + // ============================================================ + async getOrCreateProfile(userId: string) { + const profile = await this.prisma.expertProfile.upsert({ + where: { userId }, + create: { userId }, + update: {}, + include: { + experiences: { orderBy: { createdAt: 'desc' } }, + languages: { orderBy: { language: 'asc' } }, + projects: { orderBy: [{ fromYear: 'desc' }, { fromMonth: 'desc' }] }, + certifications: { orderBy: { issueYear: 'desc' } }, + attachments: { + select: { id: true, filename: true, mimetype: true, size: true, createdAt: true }, + orderBy: { createdAt: 'desc' }, + }, + }, + }); + + return profile; + } + + /** + * Profil-ID für einen User ermitteln (erstellt bei Bedarf). + */ + private async ensureProfileId(userId: string): Promise { + const profile = await this.prisma.expertProfile.upsert({ + where: { userId }, + create: { userId }, + update: {}, + select: { id: true }, + }); + return profile.id; + } + + // ============================================================ + // Skills + // ============================================================ + async updateSkills(userId: string, dto: UpdateSkillsDto) { + const profileId = await this.ensureProfileId(userId); + + const updated = await this.prisma.expertProfile.update({ + where: { id: profileId }, + data: { skills: dto.skills }, + select: { skills: true }, + }); + + this.logger.log(`Skills aktualisiert für User ${userId}: ${dto.skills.length} Skills`); + return updated; + } + + // ============================================================ + // Erfahrung (Experience) + // ============================================================ + async addExperience(userId: string, dto: CreateExperienceDto) { + const profileId = await this.ensureProfileId(userId); + + const experience = await this.prisma.expertExperience.create({ + data: { + expertProfileId: profileId, + area: dto.area, + years: dto.years, + ...(dto.level !== undefined && { level: dto.level }), + }, + }); + + this.logger.log(`Erfahrung hinzugefügt: ${dto.area} (${dto.years} Jahre)`); + return experience; + } + + async deleteExperience(userId: string, experienceId: string) { + await this.verifyOwnership(userId, 'expertExperience', experienceId); + + await this.prisma.expertExperience.delete({ + where: { id: experienceId }, + }); + } + + // ============================================================ + // Sprachen (Languages) + // ============================================================ + async addLanguage(userId: string, dto: CreateLanguageDto) { + const profileId = await this.ensureProfileId(userId); + + const language = await this.prisma.expertLanguage.create({ + data: { + expertProfileId: profileId, + language: dto.language, + level: dto.level, + }, + }); + + this.logger.log(`Sprache hinzugefügt: ${dto.language} (${dto.level})`); + return language; + } + + async deleteLanguage(userId: string, languageId: string) { + await this.verifyOwnership(userId, 'expertLanguage', languageId); + + await this.prisma.expertLanguage.delete({ + where: { id: languageId }, + }); + } + + // ============================================================ + // Projekte (Projects) + // ============================================================ + async addProject(userId: string, dto: CreateProjectDto) { + const profileId = await this.ensureProfileId(userId); + + const project = await this.prisma.expertProject.create({ + data: { + expertProfileId: profileId, + fromMonth: dto.fromMonth, + fromYear: dto.fromYear, + toMonth: dto.isCurrent ? null : (dto.toMonth ?? null), + toYear: dto.isCurrent ? null : (dto.toYear ?? null), + isCurrent: dto.isCurrent ?? false, + role: dto.role, + ...(dto.tasks !== undefined && { tasks: dto.tasks }), + ...(dto.company !== undefined && { company: dto.company }), + ...(dto.companySize !== undefined && { companySize: dto.companySize }), + ...(dto.industry !== undefined && { industry: dto.industry }), + }, + }); + + this.logger.log(`Projekt hinzugefügt: ${dto.role} (${dto.fromMonth}/${dto.fromYear})`); + return project; + } + + async updateProject(userId: string, projectId: string, dto: UpdateProjectDto) { + await this.verifyOwnership(userId, 'expertProject', projectId); + + const project = await this.prisma.expertProject.update({ + where: { id: projectId }, + data: { + ...(dto.fromMonth !== undefined && { fromMonth: dto.fromMonth }), + ...(dto.fromYear !== undefined && { fromYear: dto.fromYear }), + ...(dto.isCurrent !== undefined && { + isCurrent: dto.isCurrent, + toMonth: dto.isCurrent ? null : (dto.toMonth ?? undefined), + toYear: dto.isCurrent ? null : (dto.toYear ?? undefined), + }), + ...(!dto.isCurrent && dto.toMonth !== undefined && { toMonth: dto.toMonth }), + ...(!dto.isCurrent && dto.toYear !== undefined && { toYear: dto.toYear }), + ...(dto.role !== undefined && { role: dto.role }), + ...(dto.tasks !== undefined && { tasks: dto.tasks }), + ...(dto.company !== undefined && { company: dto.company }), + ...(dto.companySize !== undefined && { companySize: dto.companySize }), + ...(dto.industry !== undefined && { industry: dto.industry }), + }, + }); + + return project; + } + + async deleteProject(userId: string, projectId: string) { + await this.verifyOwnership(userId, 'expertProject', projectId); + + await this.prisma.expertProject.delete({ + where: { id: projectId }, + }); + } + + // ============================================================ + // Zertifizierungen (Certifications) + // ============================================================ + async addCertification(userId: string, dto: CreateCertificationDto) { + const profileId = await this.ensureProfileId(userId); + + const certification = await this.prisma.expertCertification.create({ + data: { + expertProfileId: profileId, + title: dto.title, + issuingBody: dto.issuingBody, + ...(dto.website !== undefined && { website: dto.website }), + issueYear: dto.issueYear, + }, + }); + + this.logger.log(`Zertifizierung hinzugefügt: ${dto.title}`); + return certification; + } + + async updateCertification(userId: string, certificationId: string, dto: UpdateCertificationDto) { + await this.verifyOwnership(userId, 'expertCertification', certificationId); + + const certification = await this.prisma.expertCertification.update({ + where: { id: certificationId }, + data: { + ...(dto.title !== undefined && { title: dto.title }), + ...(dto.issuingBody !== undefined && { issuingBody: dto.issuingBody }), + ...(dto.website !== undefined && { website: dto.website }), + ...(dto.issueYear !== undefined && { issueYear: dto.issueYear }), + }, + }); + + return certification; + } + + async deleteCertification(userId: string, certificationId: string) { + await this.verifyOwnership(userId, 'expertCertification', certificationId); + + await this.prisma.expertCertification.delete({ + where: { id: certificationId }, + }); + } + + // ============================================================ + // Profilanlagen (Attachments) + // ============================================================ + async uploadAttachment(userId: string, dto: UploadAttachmentDto) { + const profileId = await this.ensureProfileId(userId); + + const attachment = await this.prisma.expertAttachment.create({ + data: { + expertProfileId: profileId, + filename: dto.filename, + mimetype: dto.mimetype, + size: dto.size, + data: dto.data, + }, + select: { id: true, filename: true, mimetype: true, size: true, createdAt: true }, + }); + + this.logger.log(`Anhang hochgeladen: ${dto.filename} (${dto.size} Bytes)`); + return attachment; + } + + async downloadAttachment(userId: string, attachmentId: string) { + const attachment = await this.prisma.expertAttachment.findUnique({ + where: { id: attachmentId }, + include: { expertProfile: { select: { userId: true } } }, + }); + + if (!attachment) { + throw new NotFoundException('Anhang nicht gefunden'); + } + + if (attachment.expertProfile.userId !== userId) { + throw new ForbiddenException('Kein Zugriff auf diesen Anhang'); + } + + return { + id: attachment.id, + filename: attachment.filename, + mimetype: attachment.mimetype, + size: attachment.size, + data: attachment.data, + }; + } + + async deleteAttachment(userId: string, attachmentId: string) { + const attachment = await this.prisma.expertAttachment.findUnique({ + where: { id: attachmentId }, + include: { expertProfile: { select: { userId: true } } }, + }); + + if (!attachment) { + throw new NotFoundException('Anhang nicht gefunden'); + } + + if (attachment.expertProfile.userId !== userId) { + throw new ForbiddenException('Kein Zugriff auf diesen Anhang'); + } + + await this.prisma.expertAttachment.delete({ + where: { id: attachmentId }, + }); + + this.logger.log(`Anhang gelöscht: ${attachment.filename}`); + } + + // ============================================================ + // Hilfsfunktion: Ownership-Check + // ============================================================ + private async verifyOwnership( + userId: string, + model: 'expertExperience' | 'expertLanguage' | 'expertProject' | 'expertCertification', + entityId: string, + ): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const entity = await (this.prisma[model] as any).findUnique({ + where: { id: entityId }, + include: { expertProfile: { select: { userId: true } } }, + }); + + if (!entity) { + throw new NotFoundException('Eintrag nicht gefunden'); + } + + if (entity.expertProfile.userId !== userId) { + throw new ForbiddenException('Kein Zugriff auf diesen Eintrag'); + } + } +} diff --git a/packages/core-service/src/main.ts b/packages/core-service/src/main.ts index 5857ca8..05aa80f 100644 --- a/packages/core-service/src/main.ts +++ b/packages/core-service/src/main.ts @@ -16,8 +16,8 @@ async function bootstrap(): Promise { app.use(helmet()); app.use(cookieParser()); - // Body size limit erhoehen fuer Base64 Avatar-Uploads (Standard ~100KB) - app.use(json({ limit: '1mb' })); + // Body size limit für Base64-Uploads (Avatare, Profilanlagen bis 10MB) + app.use(json({ limit: '12mb' })); // CORS const corsOrigins = process.env.CORS_ORIGINS?.split(',') ?? [ diff --git a/packages/frontend/src/components/Modal.module.css b/packages/frontend/src/components/Modal.module.css new file mode 100644 index 0000000..5cb744c --- /dev/null +++ b/packages/frontend/src/components/Modal.module.css @@ -0,0 +1,86 @@ +.overlay { + position: fixed; + inset: 0; + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.5); + padding: 1rem; + animation: fadeIn 0.15s ease-out; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.container { + background: var(--color-bg-card, #fff); + border-radius: var(--radius-md, 8px); + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + width: 100%; + max-height: 90vh; + display: flex; + flex-direction: column; + animation: slideUp 0.15s ease-out; +} + +@keyframes slideUp { + from { transform: translateY(20px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } +} + +.header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1.25rem 1.5rem; + border-bottom: 1px solid var(--color-border, #e5e7eb); + flex-shrink: 0; +} + +.title { + font-size: 1.125rem; + font-weight: 600; + margin: 0; +} + +.closeButton { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + font-size: 1.5rem; + line-height: 1; + color: var(--color-text-muted, #9ca3af); + background: none; + border: none; + border-radius: var(--radius-sm, 4px); + cursor: pointer; + transition: all 0.15s; +} + +.closeButton:hover { + background: var(--color-bg, #f3f4f6); + color: var(--color-text, #111827); +} + +.body { + padding: 1.5rem; + overflow-y: auto; + flex: 1; +} + +@media (max-width: 640px) { + .overlay { + padding: 0; + align-items: flex-end; + } + + .container { + max-height: 95vh; + border-radius: var(--radius-md, 8px) var(--radius-md, 8px) 0 0; + } +} diff --git a/packages/frontend/src/components/Modal.tsx b/packages/frontend/src/components/Modal.tsx new file mode 100644 index 0000000..5cb31aa --- /dev/null +++ b/packages/frontend/src/components/Modal.tsx @@ -0,0 +1,60 @@ +import { useEffect, useCallback, type ReactNode } from 'react'; +import { createPortal } from 'react-dom'; +import styles from './Modal.module.css'; + +interface ModalProps { + isOpen: boolean; + onClose: () => void; + title: string; + children: ReactNode; + maxWidth?: string; +} + +export function Modal({ isOpen, onClose, title, children, maxWidth = '600px' }: ModalProps) { + const handleEscape = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose(); + }, + [onClose], + ); + + useEffect(() => { + if (isOpen) { + document.addEventListener('keydown', handleEscape); + document.body.style.overflow = 'hidden'; + } + return () => { + document.removeEventListener('keydown', handleEscape); + document.body.style.overflow = ''; + }; + }, [isOpen, handleEscape]); + + if (!isOpen) return null; + + return createPortal( +
+
e.stopPropagation()} + role="dialog" + aria-modal="true" + aria-label={title} + > +
+

{title}

+ +
+
{children}
+
+
, + document.body, + ); +} diff --git a/packages/frontend/src/profile/ExpertProfileTab.module.css b/packages/frontend/src/profile/ExpertProfileTab.module.css new file mode 100644 index 0000000..b582cb1 --- /dev/null +++ b/packages/frontend/src/profile/ExpertProfileTab.module.css @@ -0,0 +1,469 @@ +.expertContainer { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.loading { + text-align: center; + color: var(--color-text-muted); + padding: 3rem 0; + font-size: 0.9375rem; +} + +.errorBox { + background: #fef2f2; + color: var(--color-error); + padding: 1rem; + border-radius: var(--radius-md); + border: 1px solid #fecaca; + font-size: 0.875rem; +} + +/* === Section Card === */ +.section { + background: var(--color-bg-card); + border-radius: var(--radius-md); + padding: 1.5rem; + box-shadow: var(--shadow-sm); + border: 1px solid var(--color-border); +} + +.sectionHeader { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 1rem; + padding-bottom: 0.75rem; + border-bottom: 1px solid var(--color-border); +} + +.sectionTitle { + font-size: 1rem; + font-weight: 600; + margin: 0; +} + +/* === Chips/Tags === */ +.chipContainer { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 1rem; +} + +.chip { + display: inline-flex; + align-items: center; + gap: 0.375rem; + padding: 0.375rem 0.75rem; + background: var(--color-primary-light, #eff6ff); + color: var(--color-primary); + border-radius: 9999px; + font-size: 0.8125rem; + font-weight: 500; +} + +.chipRemove { + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + font-size: 1rem; + line-height: 1; + color: var(--color-primary); + background: none; + border: none; + border-radius: 50%; + cursor: pointer; + opacity: 0.6; + transition: opacity 0.15s; +} + +.chipRemove:hover { + opacity: 1; + background: rgba(0, 0, 0, 0.1); +} + +.chipInput { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.chipInput input { + padding: 0.5rem 0.75rem; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + font-size: 0.875rem; + flex: 1; + max-width: 250px; +} + +.chipInput input:focus { + outline: none; + border-color: var(--color-primary); + box-shadow: 0 0 0 3px var(--color-primary-light); +} + +/* === Entry List === */ +.entryList { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-bottom: 1rem; +} + +.entryItem { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.625rem 0.75rem; + background: var(--color-bg, #f9fafb); + border-radius: var(--radius-sm); + border: 1px solid var(--color-border); + font-size: 0.875rem; +} + +.entryInfo { + display: flex; + align-items: center; + gap: 0.75rem; + flex: 1; + min-width: 0; +} + +.entryPrimary { + font-weight: 500; +} + +.entrySecondary { + color: var(--color-text-muted); + font-size: 0.8125rem; +} + +.entryBadge { + display: inline-block; + padding: 0.125rem 0.5rem; + background: var(--color-primary-light, #eff6ff); + color: var(--color-primary); + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 500; +} + +.entryActions { + display: flex; + gap: 0.375rem; + flex-shrink: 0; + margin-left: 0.75rem; +} + +/* === Action Buttons (small) === */ +.btnIcon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + background: none; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + cursor: pointer; + font-size: 0.875rem; + color: var(--color-text-secondary); + transition: all 0.15s; +} + +.btnIcon:hover { + background: var(--color-bg); + color: var(--color-text); +} + +.btnIconDanger:hover { + background: #fef2f2; + color: var(--color-error); + border-color: #fecaca; +} + +/* === Add Form (inline) === */ +.addForm { + display: flex; + gap: 0.5rem; + align-items: flex-end; + flex-wrap: wrap; +} + +.addForm .fieldInline { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.addForm .fieldInline label { + font-size: 0.75rem; + color: var(--color-text-muted); + font-weight: 500; +} + +.addForm input, +.addForm select { + padding: 0.5rem 0.75rem; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + font-size: 0.875rem; +} + +.addForm input:focus, +.addForm select:focus { + outline: none; + border-color: var(--color-primary); + box-shadow: 0 0 0 3px var(--color-primary-light); +} + +/* === Buttons === */ +.btnPrimary { + padding: 0.5rem 1rem; + background: var(--color-primary); + color: white; + border: none; + border-radius: var(--radius-sm); + font-size: 0.8125rem; + font-weight: 600; + cursor: pointer; + transition: background 0.15s; + white-space: nowrap; +} + +.btnPrimary:hover:not(:disabled) { + background: var(--color-primary-hover); +} + +.btnPrimary:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +.btnSecondary { + padding: 0.5rem 1rem; + background: transparent; + color: var(--color-text-secondary); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + font-size: 0.8125rem; + font-weight: 500; + cursor: pointer; + transition: all 0.15s; + white-space: nowrap; +} + +.btnSecondary:hover { + background: var(--color-bg); +} + +.btnDanger { + padding: 0.5rem 1rem; + background: var(--color-error); + color: white; + border: none; + border-radius: var(--radius-sm); + font-size: 0.8125rem; + font-weight: 600; + cursor: pointer; + transition: background 0.15s; + white-space: nowrap; +} + +.btnDanger:hover:not(:disabled) { + background: #b91c1c; +} + +.btnRow { + display: flex; + gap: 0.75rem; + align-items: center; + margin-top: 0.5rem; +} + +/* === Modal Form === */ +.modalForm { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.modalField { + display: flex; + flex-direction: column; + gap: 0.375rem; +} + +.modalField label { + font-size: 0.875rem; + font-weight: 500; + color: var(--color-text); +} + +.modalField input, +.modalField select, +.modalField textarea { + padding: 0.625rem 0.75rem; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + font-size: 0.9375rem; + transition: border-color 0.15s; + font-family: inherit; +} + +.modalField input:focus, +.modalField select:focus, +.modalField textarea:focus { + outline: none; + border-color: var(--color-primary); + box-shadow: 0 0 0 3px var(--color-primary-light); +} + +.modalField input:disabled, +.modalField select:disabled { + background: var(--color-bg); + color: var(--color-text-muted); +} + +.modalField small { + color: var(--color-text-muted); + font-size: 0.75rem; +} + +.modalFieldRow { + display: flex; + gap: 1rem; +} + +.modalFieldRow .modalField { + flex: 1; +} + +.charCount { + text-align: right; + font-size: 0.75rem; + color: var(--color-text-muted); +} + +.charCountWarn { + color: var(--color-error); +} + +.checkboxRow { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.checkboxRow input[type="checkbox"] { + width: 16px; + height: 16px; + accent-color: var(--color-primary); +} + +.checkboxRow label { + font-size: 0.875rem; + color: var(--color-text); + cursor: pointer; +} + +/* === Messages === */ +.success { + background: #f0fdf4; + color: var(--color-success); + padding: 0.625rem 0.75rem; + border-radius: var(--radius-sm); + font-size: 0.8125rem; + border: 1px solid #bbf7d0; + margin-bottom: 0.75rem; +} + +.error { + background: #fef2f2; + color: var(--color-error); + padding: 0.625rem 0.75rem; + border-radius: var(--radius-sm); + font-size: 0.8125rem; + border: 1px solid #fecaca; + margin-bottom: 0.75rem; +} + +/* === Empty State === */ +.emptyState { + text-align: center; + color: var(--color-text-muted); + padding: 1.5rem 0; + font-size: 0.875rem; +} + +/* === Attachment List === */ +.attachmentItem { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.625rem 0.75rem; + background: var(--color-bg, #f9fafb); + border-radius: var(--radius-sm); + border: 1px solid var(--color-border); + font-size: 0.875rem; +} + +.attachmentInfo { + display: flex; + flex-direction: column; + gap: 0.125rem; + min-width: 0; + flex: 1; +} + +.attachmentName { + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.attachmentMeta { + font-size: 0.75rem; + color: var(--color-text-muted); +} + +.hiddenInput { + display: none; +} + +/* === Responsive === */ +@media (max-width: 640px) { + .addForm { + flex-direction: column; + align-items: stretch; + } + + .chipInput { + flex-direction: column; + align-items: stretch; + } + + .chipInput input { + max-width: 100%; + } + + .entryItem { + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + } + + .entryActions { + margin-left: 0; + } + + .modalFieldRow { + flex-direction: column; + } +} diff --git a/packages/frontend/src/profile/ExpertProfileTab.tsx b/packages/frontend/src/profile/ExpertProfileTab.tsx new file mode 100644 index 0000000..2b7df7b --- /dev/null +++ b/packages/frontend/src/profile/ExpertProfileTab.tsx @@ -0,0 +1,111 @@ +import { useState, useEffect, useCallback } from 'react'; +import api from '../api/client'; +import { SkillsSection } from './sections/SkillsSection'; +import { ExperienceSection } from './sections/ExperienceSection'; +import { LanguagesSection } from './sections/LanguagesSection'; +import { ProjectsSection } from './sections/ProjectsSection'; +import { CertificationsSection } from './sections/CertificationsSection'; +import { AttachmentsSection } from './sections/AttachmentsSection'; +import styles from './ExpertProfileTab.module.css'; + +interface ExpertExperience { + id: string; + area: string; + years: number; + level?: string | null; +} + +interface ExpertLanguage { + id: string; + language: string; + level: string; +} + +interface ExpertProject { + id: string; + fromMonth: number; + fromYear: number; + toMonth?: number | null; + toYear?: number | null; + isCurrent: boolean; + role: string; + tasks?: string | null; + company?: string | null; + companySize?: string | null; + industry?: string | null; +} + +interface ExpertCertification { + id: string; + title: string; + issuingBody: string; + website?: string | null; + issueYear: number; +} + +interface AttachmentMeta { + id: string; + filename: string; + mimetype: string; + size: number; + createdAt: string; +} + +interface ExpertProfile { + id: string; + skills: string[]; + experiences: ExpertExperience[]; + languages: ExpertLanguage[]; + projects: ExpertProject[]; + certifications: ExpertCertification[]; + attachments: AttachmentMeta[]; +} + +export type { ExpertExperience, ExpertLanguage, ExpertProject, ExpertCertification, AttachmentMeta }; + +export function ExpertProfileTab() { + const [profile, setProfile] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + + const loadProfile = useCallback(async () => { + try { + const { data } = await api.get('/expert-profile/me'); + setProfile(data); + setError(''); + } catch { + setError('Experten-Profil konnte nicht geladen werden'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + loadProfile(); + }, [loadProfile]); + + if (loading) { + return ( +
+ Experten-Profil wird geladen... +
+ ); + } + + if (error && !profile) { + return
{error}
; + } + + if (!profile) return null; + + return ( +
+ + + + + + +
+ ); +} diff --git a/packages/frontend/src/profile/ProfilePage.tsx b/packages/frontend/src/profile/ProfilePage.tsx index 5c6d54c..7817743 100644 --- a/packages/frontend/src/profile/ProfilePage.tsx +++ b/packages/frontend/src/profile/ProfilePage.tsx @@ -3,6 +3,7 @@ import { useAuth } from '../auth/AuthContext'; import api from '../api/client'; import { UserAvatar } from '../components/UserAvatar'; import { resizeImageToBase64 } from '../utils/imageUtils'; +import { ExpertProfileTab } from './ExpertProfileTab'; import styles from './ProfilePage.module.css'; type ProfileTab = 'personal' | 'expert' | 'password'; @@ -532,15 +533,8 @@ export function ProfilePage() { )} - {/* === Tab: Experten Profil (Platzhalter) === */} - {activeTab === 'expert' && ( -
-

Experten Profil

-

- Hier können Sie zukünftig Ihr Experten-Profil verwalten. -

-
- )} + {/* === Tab: Experten Profil === */} + {activeTab === 'expert' && } {/* === Tab: Passwort ändern + 2FA === */} {activeTab === 'password' && ( diff --git a/packages/frontend/src/profile/sections/AttachmentsSection.tsx b/packages/frontend/src/profile/sections/AttachmentsSection.tsx new file mode 100644 index 0000000..2a95fb9 --- /dev/null +++ b/packages/frontend/src/profile/sections/AttachmentsSection.tsx @@ -0,0 +1,165 @@ +import { useState, useRef, type ChangeEvent } from 'react'; +import api from '../../api/client'; +import type { AttachmentMeta } from '../ExpertProfileTab'; +import styles from '../ExpertProfileTab.module.css'; + +interface AttachmentsSectionProps { + attachments: AttachmentMeta[]; + onUpdate: () => Promise; +} + +function formatFileSize(bytes: number): string { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; +} + +function formatDate(dateStr: string): string { + return new Date(dateStr).toLocaleDateString('de-DE', { + day: '2-digit', month: '2-digit', year: 'numeric', + }); +} + +const ACCEPTED_TYPES = '.pdf,.jpg,.jpeg,.png,.docx,.doc'; +const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB + +export function AttachmentsSection({ attachments, onUpdate }: AttachmentsSectionProps) { + const fileInputRef = useRef(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + + const handleFileSelect = async (e: ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + if (file.size > MAX_FILE_SIZE) { + setError('Datei darf maximal 10MB groß sein'); + return; + } + + setLoading(true); + setError(''); + setSuccess(''); + + try { + const base64 = await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.onerror = reject; + reader.readAsDataURL(file); + }); + + await api.post('/expert-profile/me/attachments', { + filename: file.name, + mimetype: file.type || 'application/octet-stream', + size: file.size, + data: base64, + }); + + setSuccess(`"${file.name}" erfolgreich hochgeladen`); + await onUpdate(); + } catch { + setError('Fehler beim Hochladen'); + } finally { + setLoading(false); + if (fileInputRef.current) fileInputRef.current.value = ''; + } + }; + + const handleDownload = async (id: string, filename: string) => { + try { + const { data } = await api.get<{ data: string; mimetype: string }>( + `/expert-profile/me/attachments/${id}`, + ); + + const link = document.createElement('a'); + link.href = data.data; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } catch { + setError('Fehler beim Herunterladen'); + } + }; + + const handleDelete = async (id: string) => { + setLoading(true); + setError(''); + setSuccess(''); + try { + await api.delete(`/expert-profile/me/attachments/${id}`); + await onUpdate(); + } catch { + setError('Fehler beim Löschen'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+

Profilanlagen

+ +
+ + + + {error &&
{error}
} + {success &&
{success}
} + + {attachments.length > 0 ? ( +
+ {attachments.map((att) => ( +
+
+ {att.filename} + + {formatFileSize(att.size)} · {formatDate(att.createdAt)} + +
+
+ + +
+
+ ))} +
+ ) : ( +

+ Noch keine Anlagen hochgeladen. Unterstützte Formate: PDF, JPEG, PNG, DOCX (max. 10MB) +

+ )} +
+ ); +} diff --git a/packages/frontend/src/profile/sections/CertificationModal.tsx b/packages/frontend/src/profile/sections/CertificationModal.tsx new file mode 100644 index 0000000..a642c99 --- /dev/null +++ b/packages/frontend/src/profile/sections/CertificationModal.tsx @@ -0,0 +1,134 @@ +import { useState, useEffect, type FormEvent } from 'react'; +import { Modal } from '../../components/Modal'; +import api from '../../api/client'; +import type { ExpertCertification } from '../ExpertProfileTab'; +import styles from '../ExpertProfileTab.module.css'; + +interface CertificationModalProps { + isOpen: boolean; + onClose: () => void; + onSave: () => Promise; + certification: ExpertCertification | null; +} + +const currentYear = new Date().getFullYear(); +const YEARS = Array.from({ length: 40 }, (_, i) => currentYear - i); + +export function CertificationModal({ isOpen, onClose, onSave, certification }: CertificationModalProps) { + const [title, setTitle] = useState(''); + const [issuingBody, setIssuingBody] = useState(''); + const [website, setWebsite] = useState(''); + const [issueYear, setIssueYear] = useState(currentYear); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + useEffect(() => { + if (isOpen) { + if (certification) { + setTitle(certification.title); + setIssuingBody(certification.issuingBody); + setWebsite(certification.website ?? ''); + setIssueYear(certification.issueYear); + } else { + setTitle(''); + setIssuingBody(''); + setWebsite(''); + setIssueYear(currentYear); + } + setError(''); + } + }, [isOpen, certification]); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(''); + + const payload = { + title: title.trim(), + issuingBody: issuingBody.trim(), + ...(website.trim() && { website: website.trim() }), + issueYear, + }; + + try { + if (certification) { + await api.patch(`/expert-profile/me/certifications/${certification.id}`, payload); + } else { + await api.post('/expert-profile/me/certifications', payload); + } + await onSave(); + } catch (err: unknown) { + const apiErr = err as { response?: { data?: { message?: string } } }; + setError(apiErr.response?.data?.message ?? 'Fehler beim Speichern'); + } finally { + setLoading(false); + } + }; + + return ( + +
+ {error &&
{error}
} + +
+ + setTitle(e.target.value)} + placeholder="Welche Bezeichnung trägt Ihr Zertifikat?" + maxLength={300} + required + /> +
+ +
+ + setIssuingBody(e.target.value)} + placeholder="Welche Organisation hat Ihr Zertifikat erstellt?" + maxLength={300} + required + /> +
+ +
+ + setWebsite(e.target.value)} + placeholder="https://" + maxLength={500} + /> +
+ +
+ + +
+ +
+ + +
+
+
+ ); +} diff --git a/packages/frontend/src/profile/sections/CertificationsSection.tsx b/packages/frontend/src/profile/sections/CertificationsSection.tsx new file mode 100644 index 0000000..5344d97 --- /dev/null +++ b/packages/frontend/src/profile/sections/CertificationsSection.tsx @@ -0,0 +1,106 @@ +import { useState } from 'react'; +import type { ExpertCertification } from '../ExpertProfileTab'; +import { CertificationModal } from './CertificationModal'; +import styles from '../ExpertProfileTab.module.css'; +import api from '../../api/client'; + +interface CertificationsSectionProps { + certifications: ExpertCertification[]; + onUpdate: () => Promise; +} + +export function CertificationsSection({ certifications, onUpdate }: CertificationsSectionProps) { + const [modalOpen, setModalOpen] = useState(false); + const [editCert, setEditCert] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const handleEdit = (cert: ExpertCertification) => { + setEditCert(cert); + setModalOpen(true); + }; + + const handleAdd = () => { + setEditCert(null); + setModalOpen(true); + }; + + const handleDelete = async (id: string) => { + setLoading(true); + setError(''); + try { + await api.delete(`/expert-profile/me/certifications/${id}`); + await onUpdate(); + } catch { + setError('Fehler beim Löschen'); + } finally { + setLoading(false); + } + }; + + const handleModalClose = () => { + setModalOpen(false); + setEditCert(null); + }; + + const handleModalSave = async () => { + handleModalClose(); + await onUpdate(); + }; + + return ( +
+
+

Zertifizierungen

+ +
+ + {error &&
{error}
} + + {certifications.length > 0 ? ( +
+ {certifications.map((cert) => ( +
+
+ {cert.title} + {cert.issuingBody} + {cert.issueYear} +
+
+ + +
+
+ ))} +
+ ) : ( +

Noch keine Zertifizierungen hinzugefügt

+ )} + + +
+ ); +} diff --git a/packages/frontend/src/profile/sections/ExperienceSection.tsx b/packages/frontend/src/profile/sections/ExperienceSection.tsx new file mode 100644 index 0000000..a61b6bf --- /dev/null +++ b/packages/frontend/src/profile/sections/ExperienceSection.tsx @@ -0,0 +1,129 @@ +import { useState, type FormEvent } from 'react'; +import api from '../../api/client'; +import type { ExpertExperience } from '../ExpertProfileTab'; +import styles from '../ExpertProfileTab.module.css'; + +interface ExperienceSectionProps { + experiences: ExpertExperience[]; + onUpdate: () => Promise; +} + +export function ExperienceSection({ experiences, onUpdate }: ExperienceSectionProps) { + const [area, setArea] = useState(''); + const [years, setYears] = useState(''); + const [level, setLevel] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const handleAdd = async (e: FormEvent) => { + e.preventDefault(); + if (!area.trim() || !years) return; + + setLoading(true); + setError(''); + try { + await api.post('/expert-profile/me/experiences', { + area: area.trim(), + years: parseInt(years, 10), + ...(level && { level }), + }); + setArea(''); + setYears(''); + setLevel(''); + await onUpdate(); + } catch { + setError('Fehler beim Hinzufügen'); + } finally { + setLoading(false); + } + }; + + const handleDelete = async (id: string) => { + setLoading(true); + setError(''); + try { + await api.delete(`/expert-profile/me/experiences/${id}`); + await onUpdate(); + } catch { + setError('Fehler beim Löschen'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+

Erfahrung

+
+ + {error &&
{error}
} + + {experiences.length > 0 ? ( +
+ {experiences.map((exp) => ( +
+
+ {exp.area} + {exp.years} Jahre + {exp.level && {exp.level}} +
+
+ +
+
+ ))} +
+ ) : ( +

Noch keine Erfahrung hinzugefügt

+ )} + +
+
+ + setArea(e.target.value)} + placeholder="z.B. IT Infrastruktur" + maxLength={200} + required + /> +
+
+ + setYears(e.target.value)} + placeholder="0" + min={0} + max={60} + style={{ width: '80px' }} + required + /> +
+
+ + +
+ +
+
+ ); +} diff --git a/packages/frontend/src/profile/sections/LanguagesSection.tsx b/packages/frontend/src/profile/sections/LanguagesSection.tsx new file mode 100644 index 0000000..7cc9f96 --- /dev/null +++ b/packages/frontend/src/profile/sections/LanguagesSection.tsx @@ -0,0 +1,114 @@ +import { useState, type FormEvent } from 'react'; +import api from '../../api/client'; +import type { ExpertLanguage } from '../ExpertProfileTab'; +import styles from '../ExpertProfileTab.module.css'; + +interface LanguagesSectionProps { + languages: ExpertLanguage[]; + onUpdate: () => Promise; +} + +const LANGUAGE_LEVELS = ['Muttersprache', 'C2', 'C1', 'B2', 'B1', 'A2', 'A1']; + +export function LanguagesSection({ languages, onUpdate }: LanguagesSectionProps) { + const [language, setLanguage] = useState(''); + const [level, setLevel] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const handleAdd = async (e: FormEvent) => { + e.preventDefault(); + if (!language.trim() || !level) return; + + setLoading(true); + setError(''); + try { + await api.post('/expert-profile/me/languages', { + language: language.trim(), + level, + }); + setLanguage(''); + setLevel(''); + await onUpdate(); + } catch { + setError('Fehler beim Hinzufügen'); + } finally { + setLoading(false); + } + }; + + const handleDelete = async (id: string) => { + setLoading(true); + setError(''); + try { + await api.delete(`/expert-profile/me/languages/${id}`); + await onUpdate(); + } catch { + setError('Fehler beim Löschen'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+

Sprachen

+
+ + {error &&
{error}
} + + {languages.length > 0 ? ( +
+ {languages.map((lang) => ( +
+
+ {lang.language} + {lang.level} +
+
+ +
+
+ ))} +
+ ) : ( +

Noch keine Sprachen hinzugefügt

+ )} + +
+
+ + setLanguage(e.target.value)} + placeholder="z.B. Deutsch" + maxLength={100} + required + /> +
+
+ + +
+ +
+
+ ); +} diff --git a/packages/frontend/src/profile/sections/ProjectModal.tsx b/packages/frontend/src/profile/sections/ProjectModal.tsx new file mode 100644 index 0000000..1746ab9 --- /dev/null +++ b/packages/frontend/src/profile/sections/ProjectModal.tsx @@ -0,0 +1,243 @@ +import { useState, useEffect, type FormEvent } from 'react'; +import { Modal } from '../../components/Modal'; +import api from '../../api/client'; +import type { ExpertProject } from '../ExpertProfileTab'; +import styles from '../ExpertProfileTab.module.css'; + +interface ProjectModalProps { + isOpen: boolean; + onClose: () => void; + onSave: () => Promise; + project: ExpertProject | null; +} + +const MONTHS = [ + { value: 1, label: 'Januar' }, { value: 2, label: 'Februar' }, + { value: 3, label: 'März' }, { value: 4, label: 'April' }, + { value: 5, label: 'Mai' }, { value: 6, label: 'Juni' }, + { value: 7, label: 'Juli' }, { value: 8, label: 'August' }, + { value: 9, label: 'September' }, { value: 10, label: 'Oktober' }, + { value: 11, label: 'November' }, { value: 12, label: 'Dezember' }, +]; + +const COMPANY_SIZES = ['1-10', '11-50', '51-200', '201-500', '501-1000', '1001-5000', '5000+']; + +const INDUSTRIES = [ + 'IT-Dienstleistung', 'Software-Entwicklung', 'Cloud & Hosting', 'Telekommunikation', + 'Finanzdienstleistung', 'Versicherung', 'Gesundheitswesen', 'Pharma & Medizintechnik', + 'Automobilindustrie', 'Maschinenbau', 'Energiewirtschaft', 'Logistik & Transport', + 'Handel & E-Commerce', 'Medien & Unterhaltung', 'Bildung & Forschung', + 'Öffentlicher Sektor', 'Beratung & Consulting', 'Luft- und Raumfahrt', + 'Chemie & Werkstoffe', 'Sonstige', +]; + +const currentYear = new Date().getFullYear(); +const YEARS = Array.from({ length: 40 }, (_, i) => currentYear - i); + +export function ProjectModal({ isOpen, onClose, onSave, project }: ProjectModalProps) { + const [fromMonth, setFromMonth] = useState(1); + const [fromYear, setFromYear] = useState(currentYear); + const [toMonth, setToMonth] = useState(1); + const [toYear, setToYear] = useState(currentYear); + const [isCurrent, setIsCurrent] = useState(false); + const [role, setRole] = useState(''); + const [tasks, setTasks] = useState(''); + const [company, setCompany] = useState(''); + const [companySize, setCompanySize] = useState(''); + const [industry, setIndustry] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + useEffect(() => { + if (isOpen) { + if (project) { + setFromMonth(project.fromMonth); + setFromYear(project.fromYear); + setToMonth(project.toMonth ?? 1); + setToYear(project.toYear ?? currentYear); + setIsCurrent(project.isCurrent); + setRole(project.role); + setTasks(project.tasks ?? ''); + setCompany(project.company ?? ''); + setCompanySize(project.companySize ?? ''); + setIndustry(project.industry ?? ''); + } else { + setFromMonth(1); + setFromYear(currentYear); + setToMonth(1); + setToYear(currentYear); + setIsCurrent(false); + setRole(''); + setTasks(''); + setCompany(''); + setCompanySize(''); + setIndustry(''); + } + setError(''); + } + }, [isOpen, project]); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(''); + + const payload = { + fromMonth, + fromYear, + ...(!isCurrent && { toMonth, toYear }), + isCurrent, + role: role.trim(), + ...(tasks.trim() && { tasks: tasks.trim() }), + ...(company.trim() && { company: company.trim() }), + ...(companySize && { companySize }), + ...(industry && { industry }), + }; + + try { + if (project) { + await api.patch(`/expert-profile/me/projects/${project.id}`, payload); + } else { + await api.post('/expert-profile/me/projects', payload); + } + await onSave(); + } catch (err: unknown) { + const apiErr = err as { response?: { data?: { message?: string } } }; + setError(apiErr.response?.data?.message ?? 'Fehler beim Speichern'); + } finally { + setLoading(false); + } + }; + + return ( + +
+ {error &&
{error}
} + + {/* Zeitraum von */} +
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ setIsCurrent(e.target.checked)} + /> + +
+ +
+ + setRole(e.target.value)} + placeholder="z.B. Senior DevOps Engineer" + maxLength={200} + required + /> +
+ +
+ +